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:

RolePurposeImpact
PRODUCT_TAX_RATEDefines the tax percentage applicable to the product (e.g. 20, 5.5)Used to compute the tax-inclusive price at import time
PRODUCT_TAX_CODEFiscal 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_RATEDefines the tax percentage applicable to shipping costsUsed to compute tax-inclusive shipping price at import time
SHIPPING_TAX_CODEFiscal code for shipping taxesUsed 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 via PUT /v1/custom-field-roles to 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:

CalculationFormula
Product tax amountprice_excl_tax × (PRODUCT_TAX_RATE / 100)
Product price incl. taxprice_excl_tax + tax_amount
Shipping tax amountshipping_price_excl_tax × (SHIPPING_TAX_RATE / 100)
Shipping price incl. taxshipping_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
ScenarioBehavior
Role not assignedPlatform 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 existThe 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 existAll 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 CFThe custom field becomes required. Existing offers are not deactivated.
Role reassigned to a different custom fieldThe 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-fields with a role). Mapping a role to an existing custom field via PUT /v1/custom-field-roles does 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

ScenarioBehavior
Role not assignedPlatform operates in tax-exclusive mode. TTC prices equal HT prices.
Role assigned before any offers existThe mapped custom field becomes required. Offers without this field are rejected at import.
Role assigned after offers existThe tax code is used in order exports and invoicing.

SHIPPING_TAX_RATE

ScenarioBehavior
Role not assignedNo tax calculation on shipping. Shipping TTC prices equal HT prices.
CF created with role and no offers existThe 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 existAll existing offers are deactivated.
Role mapped via PUT /v1/custom-field-roles to an existing CFThe custom field becomes required. Existing offers are not deactivated.
Role reassigned to a different custom fieldThe 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 to INACTIVE effectively 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.

TagScopeDescription
Custom Fields AdministrationAdmin (dj-client: OPERATOR)CRUD operations on custom fields (create, update, update status, get, list)
Custom Field Roles AdministrationAdmin (dj-client: OPERATOR)Assign a role to a custom field, list role mappings
Custom FieldsFront Office (Shop)Read-only access to custom fields for front-facing applications

Known operationId:

OperationIdMethodDescriptionSource
CUSTOM-FIELD-550GETList 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:

  1. POST /v1/custom-fields — create the CF (no role)
  2. PUT /v1/custom-field-roles — map PRODUCT_TAX_RATE to the CF
  3. Existing offers remain active
  4. The custom field becomes required for all future imports
  5. 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):

  1. All 500 existing offers are immediately deactivated (status set to INACTIVE)
  2. 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: The CF_ 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:

ModeBehaviorUse 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)
FULLEmpty 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

  1. 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.

  2. Use the correct data types — Tax rate custom fields should be LIST_NUMBER (e.g. 20 for 20%). Tax code custom fields should be TEXT.

  3. Configure rounding rules early — Set up your preferred rounding mode (down, up, nearest) in Settings > Tax Rounding before the first import.

  4. Keep tax custom fields ACTIVE — Setting a tax role custom field to INACTIVE suspends all tax calculations for that type. Only do this intentionally.

  5. 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.

  6. 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

MistakeConsequenceHow to avoid
Creating a CF with a tax rate role when offers already existAll existing offers are deactivatedUse 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 valueOffer 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 rateTax calculation fails or produces incorrect resultsUse LIST_NUMBER type for rate fields, provide values like "20" not "20%"
Assigning a tax role to a non-Offer entity custom fieldRole assignment fails (entity-role incompatibility)Only assign tax roles to custom fields with entityType: OFFER
Not creating the custom field in DJUST before importingOffer import is rejected because the custom field does not exist in DJUSTAlways 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).