Tax Management with Custom Field Roles
Introduction
In DJUST, tax management on offers relies on a mechanism called Custom Field Roles. Rather than having hard-coded tax fields, DJUST uses a flexible approach: you create standard custom fields on the Offer entity, then assign a predefined role to each one. This role tells the platform which custom field should be used for tax calculations, tax labeling, or other specific business logic.
This guide explains how to configure and use the four tax-related roles available on offers, how they impact offer imports, and how the platform computes tax-inclusive prices.
Warning: Creating a custom field with a tax rate role in a single operation will deactivate all existing offers. Plan your custom field structure carefully before linking roles. See Business Rules for details.
Key Concepts
What is a Custom Field Role?
A Custom Field in DJUST is an extensible attribute you can attach to various entities (Offers, Prices, Orders, Accounts, etc.). When a custom field is associated with a role, it gains predefined business logic enforced by the platform.
Roles are:
- Predefined — you cannot create new roles; you select from a fixed list
- Entity-scoped — a tax role can only be assigned to an Offer custom field, not to an Account or Order custom field
- Exclusive — each custom field can hold at most one role
- Reassignable — a role mapping can be updated or cleared via
PUT /v1/custom-field-roles
Tax-Related Roles for Offers
DJUST provides four tax-related roles that can be assigned to Offer custom fields:
| Role | Purpose | Impact |
|---|---|---|
PRODUCT_TAX_RATE | Defines the tax percentage applicable to the product (e.g. 20, 5.5) | Used to compute the tax-inclusive price at import time |
PRODUCT_TAX_CODE | Fiscal code assigned to the product for tax identification (e.g. VAT_STANDARD, VAT_REDUCED) | Used in invoicing, order exports, and front-end display |
SHIPPING_TAX_RATE | Defines the tax percentage applicable to shipping costs | Used to compute tax-inclusive shipping price at import time |
SHIPPING_TAX_CODE | Fiscal code for shipping taxes | Used in invoicing and order exports |
Tip: The tax rate roles drive price calculations, while the tax code roles are used for labeling and external system integration (e.g. order exports, invoicing).
How It Works: The Big Picture
flowchart LR
%% Styles (Readme)
classDef create fill:#e8f1ff,stroke:#2f6feb,stroke-width:2px,color:#0b3d91;
classDef read fill:#ede9fe,stroke:#7c3aed,stroke-width:2px,color:#1e1b4b;
classDef update fill:#e0f7fa,stroke:#06b6d4,stroke-width:2px,color:#0c4a6e;
classDef add fill:#ecfdf5,stroke:#10b981,stroke-width:2px,color:#064e3b;
classDef remove fill:#fee2e2,stroke:#ef4444,stroke-width:2px,color:#7f1d1d;
classDef decision fill:#fff4e5,stroke:#f59e0b,stroke-width:2px,color:#7a3e00;
classDef place fill:#dcfce7,stroke:#16a34a,stroke-width:2px,color:#14532d;
classDef sys fill:#f2f4f7,stroke:#475569,stroke-width:2px,color:#111827;
classDef ok fill:#ecfdf5,stroke:#10b981,stroke-width:2px,color:#064e3b;
classDef stop fill:#fee2e2,stroke:#ef4444,stroke-width:2px,color:#7f1d1d;
A["📘 Create Custom Field<br>on Offer entity"]:::create
B["➕ Assign a tax role<br>e.g. PRODUCT_TAX_RATE"]:::add
C["📦 Import offers<br>with tax values"]:::update
D{{"🧾 Role assigned?"}}:::decision
D2["✅ Tax-inclusive price<br>computed at import"]:::place
D3["➖ Tax-exclusive mode<br>no TTC calculation"]:::sys
A --> B --> C --> D
D -->|Yes| D2
D -->|No| D3
style A rx:8,ry:8
style B rx:8,ry:8
style C rx:8,ry:8
style D2 rx:8,ry:8
style D3 rx:8,ry:8
Typical Workflow
Step 1: Create the Custom Fields
Before assigning roles, you must create the custom fields on the Offer entity via the Back Office or the Admin API.
Navigate to Settings > Custom Fields, select the Offer entity, and create a custom field for each tax attribute you need (e.g. product_tax_rate, product_tax_code).
Via API:
POST /v1/custom-fields
dj-client: OPERATOR
dj-api-key: {{apiKey}}{
"code": "product_tax_rate",
"label": "Product Tax Rate",
"entityType": "OFFER",
"type": "LIST_NUMBER",
"required": false
}
Tip: This workflow (create the CF first without a role, then map the role in Step 2) is the recommended approach. It does not trigger offer deactivation, because the deactivation logic only applies when creating a CF with a role in a single operation.
Step 2: Assign a Role to Each Custom Field
Once the custom fields exist, navigate to Settings > Custom Field Roles in the Back Office, and map each role to the corresponding custom field.
Via API:
PUT /v1/custom-field-roles
dj-client: OPERATOR
dj-api-key: {{apiKey}}{
"role": "PRODUCT_TAX_RATE",
"customFieldId": "cf_product_tax_rate_id"
}
Tip: Mapping a role viaPUT /v1/custom-field-rolesto an existing custom field does not deactivate existing offers. This is the safe way to set up tax roles on a live catalog.
Step 3: Import Offers with Tax Values
Once roles are configured, every offer import must include the mapped custom field values. The platform will use these values to compute tax-inclusive prices automatically at import time.
Example offer import payload with tax custom fields:
{
"externalId": "OFFER-001",
"variantId": "VAR-12345",
"supplierId": "SUP-001",
"storeId": "STORE-FR",
"price": 100.00,
"currency": "EUR",
"stock": 250,
"shippingPrice": 5.00,
"customFieldValues": [
{
"customFieldId": "cf_product_tax_rate_id",
"value": "20"
},
{
"customFieldId": "cf_product_tax_code_id",
"value": "VAT_STANDARD"
},
{
"customFieldId": "cf_shipping_tax_rate_id",
"value": "20"
},
{
"customFieldId": "cf_shipping_tax_code_id",
"value": "VAT_STANDARD"
}
]
}Step 4: Platform Computes Tax-Inclusive Prices
At import time, DJUST automatically computes the tax-inclusive prices:
| Calculation | Formula |
|---|---|
| Product tax amount | price_excl_tax × (PRODUCT_TAX_RATE / 100) |
| Product price incl. tax | price_excl_tax + tax_amount |
| Shipping tax amount | shipping_price_excl_tax × (SHIPPING_TAX_RATE / 100) |
| Shipping price incl. tax | shipping_price_excl_tax + shipping_tax_amount |
Tip: Rounding rules are configured at the platform level via Settings > Tax Rounding. Available modes: round down, round up, round to nearest (applied at the cent level).
Business Rules by Role
PRODUCT_TAX_RATE
flowchart LR
%% Styles (Readme)
classDef create fill:#e8f1ff,stroke:#2f6feb,stroke-width:2px,color:#0b3d91;
classDef read fill:#ede9fe,stroke:#7c3aed,stroke-width:2px,color:#1e1b4b;
classDef update fill:#e0f7fa,stroke:#06b6d4,stroke-width:2px,color:#0c4a6e;
classDef add fill:#ecfdf5,stroke:#10b981,stroke-width:2px,color:#064e3b;
classDef remove fill:#fee2e2,stroke:#ef4444,stroke-width:2px,color:#7f1d1d;
classDef decision fill:#fff4e5,stroke:#f59e0b,stroke-width:2px,color:#7a3e00;
classDef place fill:#dcfce7,stroke:#16a34a,stroke-width:2px,color:#14532d;
classDef sys fill:#f2f4f7,stroke:#475569,stroke-width:2px,color:#111827;
classDef ok fill:#ecfdf5,stroke:#10b981,stroke-width:2px,color:#064e3b;
classDef stop fill:#fee2e2,stroke:#ef4444,stroke-width:2px,color:#7f1d1d;
R{{"PRODUCT_TAX_RATE<br>how assigned?"}}:::decision
R -->|Not assigned| A["Tax-exclusive mode<br>TTC = HT"]:::sys
R -->|CF created with role<br>offers exist| C["All existing offers<br>are deactivated"]:::stop
R -->|Role mapped via PUT<br>to existing CF| B["No deactivation<br>CF becomes required"]:::add
B --> E["TTC = HT + HT x rate<br>Computed at import"]:::place
C --> E
style R rx:8,ry:8
style A rx:8,ry:8
style B rx:8,ry:8
style C rx:8,ry:8
style E rx:8,ry:8
| Scenario | Behavior |
|---|---|
| Role not assigned | Platform operates in tax-exclusive mode. Tax-inclusive prices (TTC) are set equal to the tax-exclusive prices (HT). |
| CF created with role and no offers exist | The mapped custom field becomes required. Offers imported without this field are rejected. TTC prices are computed at import. |
| CF created with role and offers already exist | All existing offers are deactivated (set to INACTIVE). The custom field becomes required for future imports. |
Role mapped via PUT /v1/custom-field-roles to an existing CF | The custom field becomes required. Existing offers are not deactivated. |
| Role reassigned to a different custom field | The previous mapping is cleared and the new one takes effect. Existing offers are not deactivated by the remapping. |
Warning: Offer deactivation is only triggered when a custom field is created with a tax rate role in a single operation (POST /v1/custom-fieldswith a role). Mapping a role to an existing custom field viaPUT /v1/custom-field-rolesdoes not deactivate offers. This is why the recommended workflow is to create the CF first (without a role), then map the role separately.
PRODUCT_TAX_CODE
| Scenario | Behavior |
|---|---|
| Role not assigned | Platform operates in tax-exclusive mode. TTC prices equal HT prices. |
| Role assigned before any offers exist | The mapped custom field becomes required. Offers without this field are rejected at import. |
| Role assigned after offers exist | The tax code is used in order exports and invoicing. |
SHIPPING_TAX_RATE
| Scenario | Behavior |
|---|---|
| Role not assigned | No tax calculation on shipping. Shipping TTC prices equal HT prices. |
| CF created with role and no offers exist | The mapped custom field becomes required. Offers imported without this field are rejected. Shipping TTC = Shipping HT + (Shipping HT x rate). |
| CF created with role and offers already exist | All existing offers are deactivated. |
Role mapped via PUT /v1/custom-field-roles to an existing CF | The custom field becomes required. Existing offers are not deactivated. |
| Role reassigned to a different custom field | The previous mapping is cleared and the new one takes effect. Existing offers are not deactivated by the remapping. |
SHIPPING_TAX_CODE
Works identically to PRODUCT_TAX_CODE but applies to shipping tax labeling.
Inactive Custom Fields with Roles
A custom field assigned to a role can be set to INACTIVE status. When this happens:
- The platform behaves as if no custom field is mapped to that role
- Tax calculations linked to that role are suspended
- Re-activating the custom field (setting status back to
ACTIVE) restores the role behavior
Warning: Setting a tax-role custom field toINACTIVEeffectively switches the platform to tax-exclusive mode for that specific tax type. This can impact front-end price display and invoicing.
Key Endpoints Involved
The following API tags group the endpoints related to custom fields and their roles. Refer to the DJUST API reference for the full list of routes and operationIds.
| Tag | Scope | Description |
|---|---|---|
| Custom Fields Administration | Admin (dj-client: OPERATOR) | CRUD operations on custom fields (create, update, update status, get, list) |
| Custom Field Roles Administration | Admin (dj-client: OPERATOR) | Assign a role to a custom field, list role mappings |
| Custom Fields | Front Office (Shop) | Read-only access to custom fields for front-facing applications |
Known operationId:
| OperationId | Method | Description | Source |
|---|---|---|---|
CUSTOM-FIELD-550 | GET | List custom fields (front-facing, paginated) | Confirmed in API evolution tracker |
Warning: The admin operationIds for Custom Fields and Custom Field Roles endpoints are not yet consolidated in the API contracts at the time of writing. Check the latest DJUST API reference for up-to-date operationIds.
Offer Import Flow with Tax Roles
sequenceDiagram
participant Integrator
participant DJUST API
participant Tax Engine
Note over Integrator,Tax Engine: Prerequisites: Custom fields created + roles assigned
Integrator->>DJUST API: POST /v1/offers (with customFieldValues)
DJUST API->>DJUST API: Validate required custom fields (role = required)
alt Missing required tax field
DJUST API-->>Integrator: 422 - Required field(s) missing (F-E-001)
end
DJUST API->>Tax Engine: Compute TTC from HT + tax rate
Tax Engine->>Tax Engine: Apply rounding rules
Tax Engine-->>DJUST API: TTC prices computed
DJUST API-->>Integrator: 201 Created (offer with TTC prices)
Example Scenarios
Scenario 1: Setting Up Tax Roles from Scratch
A new DJUST tenant wants to manage product and shipping taxes on offers before importing their catalog.
Step 1 — Create the custom fields:
// POST /v1/custom-fields (dj-client: OPERATOR)
[
{ "code": "tax_rate", "label": "Product Tax Rate", "entityType": "OFFER", "type": "LIST_NUMBER" },
{ "code": "tax_code", "label": "Product Tax Code", "entityType": "OFFER", "type": "TEXT" },
{ "code": "shipping_tax_rate", "label": "Shipping Tax Rate", "entityType": "OFFER", "type": "LIST_NUMBER" },
{ "code": "shipping_tax_code", "label": "Shipping Tax Code", "entityType": "OFFER", "type": "TEXT" }
]Step 2 — Map roles to custom fields:
// PUT /v1/custom-field-roles (dj-client: OPERATOR)
{ "role": "PRODUCT_TAX_RATE", "customFieldId": "cf_tax_rate_id" }// PUT /v1/custom-field-roles (dj-client: OPERATOR)
{ "role": "PRODUCT_TAX_CODE", "customFieldId": "cf_tax_code_id" }// PUT /v1/custom-field-roles (dj-client: OPERATOR)
{ "role": "SHIPPING_TAX_RATE", "customFieldId": "cf_shipping_tax_rate_id" }// PUT /v1/custom-field-roles (dj-client: OPERATOR)
{ "role": "SHIPPING_TAX_CODE", "customFieldId": "cf_shipping_tax_code_id" }Step 3 — Import offers with all tax fields:
// POST /v1/offers (dj-client: OPERATOR)
{
"externalId": "OFFER-WIDGET-001",
"variantId": "VAR-WIDGET",
"supplierId": "SUP-ACME",
"storeId": "STORE-FR",
"price": 45.00,
"shippingPrice": 8.50,
"currency": "EUR",
"stock": 100,
"customFieldValues": [
{ "customFieldId": "cf_tax_rate_id", "value": "20" },
{ "customFieldId": "cf_tax_code_id", "value": "VAT_20" },
{ "customFieldId": "cf_shipping_tax_rate_id", "value": "20" },
{ "customFieldId": "cf_shipping_tax_code_id", "value": "VAT_20" }
]
}Result:
- Product price incl. tax:
45.00 + (45.00 × 0.20) = 54.00 EUR - Shipping price incl. tax:
8.50 + (8.50 × 0.20) = 10.20 EUR
Scenario 2: Adding Tax Roles to an Existing Catalog
An operator has 500 active offers but has never configured tax roles. They want to add PRODUCT_TAX_RATE.
Safe approach (recommended): Create the custom field first without a role, then map the role via PUT /v1/custom-field-roles:
POST /v1/custom-fields— create the CF (no role)PUT /v1/custom-field-roles— mapPRODUCT_TAX_RATEto the CF- Existing offers remain active
- The custom field becomes required for all future imports
- Existing offers will need to be re-imported to populate the tax rate value and compute TTC prices
Risky approach: Create a new custom field with the role in a single operation (POST /v1/custom-fields with a role field):
- All 500 existing offers are immediately deactivated (status set to
INACTIVE) - The operator must re-import all offers with the tax rate value to reactivate them
Warning: Only the single-operation approach (creating a CF with a role) triggers mass deactivation. Always prefer the two-step workflow on a live catalog.
Scenario 3: Temporarily Disabling Tax Calculation
An operator needs to temporarily suspend tax calculations while migrating tax codes.
Approach: Set the custom field linked to PRODUCT_TAX_RATE to INACTIVE status:
PATCH /v1/custom-fields/{customFieldId}
dj-client: OPERATOR
dj-api-key: {{apiKey}}{
"status": "INACTIVE"
}Effect: The platform behaves as if PRODUCT_TAX_RATE is not assigned. Tax-inclusive prices are no longer computed. Once the migration is complete, set the custom field back to ACTIVE.
Importing Offers with Tax Fields via the Data Hub
In addition to the REST API, offers (including their tax-related custom field values) can be imported through the Data Hub using CSV files, either via SFTP or the API connector.
CSV Import via SFTP
When importing offers via CSV through SFTP, custom fields are automatically identified using the CF_ prefix in column headers. No manual mapping is required in the job configuration.
Example CSV structure:
stockId,supplierId,variantId,stock,price,shippingPrice,CF_product_tax_rate,CF_product_tax_code,CF_shipping_tax_rate,CF_shipping_tax_code
OFFER-001,SUP-ACME,VAR-WIDGET,100,45.00,8.50,20,VAT_20,20,VAT_20
OFFER-002,SUP-ACME,VAR-GADGET,50,120.00,12.00,5.5,VAT_REDUCED,20,VAT_20
Warning: TheCF_prefix is mandatory for SFTP CSV imports. Without it, the column will not be recognized as a custom field and the value will be ignored — which may cause the offer to be rejected if the field is required by a tax role.
CSV Import via the API Connector
When using the API connector to import CSV files, custom fields must be manually mapped in the job configuration within the Back Office. The CF_ prefix convention does not apply here — instead, you configure the mapping between your CSV columns and the DJUST custom fields directly in the import job settings.
Import Modes: PARTIAL vs FULL
The Data Hub supports two import modes for custom fields, configurable per job:
| Mode | Behavior | Use case |
|---|---|---|
| PARTIAL (default) | Only non-null values are updated. Empty or absent values in the CSV are ignored — existing values are preserved. | Incremental updates (e.g. updating stock without touching tax fields) |
| FULL | Empty values in the CSV overwrite existing values. Absent columns are preserved. | Full catalog synchronization where you want to ensure the CSV is the source of truth |
Warning: In FULL mode, leaving a tax custom field column empty in your CSV will erase the existing value on the offer. If the field is required by a tax role, this will cause the offer to be rejected or deactivated on the next validation pass.
Same Business Rules Apply
Regardless of the import channel (API, SFTP, or API connector), the same tax role business rules are enforced:
- If a tax role is assigned and the custom field is required, any offer missing the value is rejected
- Tax-inclusive prices (TTC) are computed automatically at import time based on the tax rate values
- Rounding rules configured in Settings > Tax Rounding apply to all import channels
flowchart LR
%% Styles (Readme)
classDef create fill:#e8f1ff,stroke:#2f6feb,stroke-width:2px,color:#0b3d91;
classDef read fill:#ede9fe,stroke:#7c3aed,stroke-width:2px,color:#1e1b4b;
classDef update fill:#e0f7fa,stroke:#06b6d4,stroke-width:2px,color:#0c4a6e;
classDef add fill:#ecfdf5,stroke:#10b981,stroke-width:2px,color:#064e3b;
classDef remove fill:#fee2e2,stroke:#ef4444,stroke-width:2px,color:#7f1d1d;
classDef decision fill:#fff4e5,stroke:#f59e0b,stroke-width:2px,color:#7a3e00;
classDef place fill:#dcfce7,stroke:#16a34a,stroke-width:2px,color:#14532d;
classDef sys fill:#f2f4f7,stroke:#475569,stroke-width:2px,color:#111827;
classDef ok fill:#ecfdf5,stroke:#10b981,stroke-width:2px,color:#064e3b;
classDef stop fill:#fee2e2,stroke:#ef4444,stroke-width:2px,color:#7f1d1d;
A["📦 Offer data source"]:::sys
B["🔗 REST API<br>POST /v1/offers"]:::create
C["📄 SFTP CSV<br>CF_ prefix columns"]:::create
D["🔌 API Connector<br>Manual mapping"]:::create
E{{"🧾 Tax role<br>assigned?"}}:::decision
F["✅ TTC computed<br>Offer created"]:::place
G["➖ No TTC<br>Tax-exclusive"]:::sys
A --> B
A --> C
A --> D
B --> E
C --> E
D --> E
E -->|Yes + value present| F
E -->|No role| G
style A rx:8,ry:8
style B rx:8,ry:8
style C rx:8,ry:8
style D rx:8,ry:8
style F rx:8,ry:8
style G rx:8,ry:8
Best Practices
-
Use the two-step workflow — Create custom fields first (without a role), then map roles via
PUT /v1/custom-field-roles. This avoids triggering mass offer deactivation. Creating a CF with a role in a single operation will deactivate all existing offers. -
Use the correct data types — Tax rate custom fields should be
LIST_NUMBER(e.g.20for 20%). Tax code custom fields should beTEXT. -
Configure rounding rules early — Set up your preferred rounding mode (down, up, nearest) in Settings > Tax Rounding before the first import.
-
Keep tax custom fields ACTIVE — Setting a tax role custom field to
INACTIVEsuspends all tax calculations for that type. Only do this intentionally. -
Include all four tax fields in imports — Even if you only need product tax rates, consider setting up all four roles for completeness. Missing shipping tax fields mean shipping prices remain tax-exclusive.
-
Test with a small import first — Before bulk-importing thousands of offers, test with a single offer to verify that TTC prices are computed correctly.
Common Mistakes
| Mistake | Consequence | How to avoid |
|---|---|---|
| Creating a CF with a tax rate role when offers already exist | All existing offers are deactivated | Use the two-step workflow: create the CF first, then map the role via PUT /v1/custom-field-roles |
| Importing offers without the required tax custom field value | Offer is rejected (HTTP 422) | Ensure all required custom field values are included in the payload |
| Using a text value instead of a numeric value for the tax rate | Tax calculation fails or produces incorrect results | Use LIST_NUMBER type for rate fields, provide values like "20" not "20%" |
| Assigning a tax role to a non-Offer entity custom field | Role assignment fails (entity-role incompatibility) | Only assign tax roles to custom fields with entityType: OFFER |
| Not creating the custom field in DJUST before importing | Offer import is rejected because the custom field does not exist in DJUST | Always create the custom field in DJUST before starting imports from any external source |
Error Reference
For the complete list of DJUST error and warning codes, refer to the dedicated page: Error / Warning codes
The most relevant codes when working with custom field roles and tax imports are F-E-001 (missing or invalid required fields) and F-E-008 (unexpected value for a field).
Updated 15 days ago
