Working with Advanced Data Merge API

This guide covers advanced techniques for the Advanced Data Merge API, building on the basics from Working with the Data Merge API, including output path variations, conditional visibility, dynamic styling, copyfitting, zip output and pre-signed URL support in CSV.

Conditional Visibility & Dynamic Styling

Make your Data Merge output react to your data. With a simple rules file, you can show or hide elements, restyle and recolor content, replace text, and resize or move frames, differently for every record, all from a single template and data file.

Overview

Traditionally, Data Merge drops the text or image from each record into fixed placeholders in your template. The layout stays the same for everyone. Only the placeholder content changes.

This feature adds a third input to a merge job: a rules file (JSON) that travels alongside your template and data.

Template (.indd / .idml) + Data (.csv) + Rules (.json) → Output (INDD / PDF / PNG / JPEG)

For each record, the merge applies the rule actions that match that record's data. A single job can then produce a different layout for each record. For example, you can show a VIP badge only for VIP customers, display a price in red when it exceeds a threshold, or show a promotional layer only for high-spend recipients.

This feature is optional. If you don't provide a rules file, the merge runs without any rules.

What you can do

Capability
Description
Conditional visibility
Show or hide a layer, a frame, or a placeholder per record.
Dynamic styling
Apply a named character or paragraph style, or change fill/stroke color, per record.
Content replacement
Replace the text of a placeholder or tagged text range per record.
Frame adjustments
Resize or move a frame per record.
Business logic
Combine conditions with AND / OR / NOT, ranges, and text matching to decide what happens.

All decisions are driven by the values in your CSV columns.

Getting started

  1. Author your template. Create the Data Merge template, including placeholders. Assign each element you intend to control a reference name: a Data Merge placeholder name, an XML tag, or a layer name (see Targets).
  2. Prepare your CSV. Ensure every column referenced by a rule exists in the CSV, even if that column is not placed anywhere in the template.
  3. Write a rules file. Define the conditions and actions in a .json file (see below).
  4. Submit the merge job. Include the rulesFile parameter, pointing to the rules file:
{
  "params": {
    "targetDocument": "template.indd",
    "dataSource": "data.csv",
    "rulesFile": "rules.json",
    "outputMediaType": "application/pdf"
  }
}

If you omit rulesFile, the merge runs without any rules.

The rules file

A rules file is a JSON document. At the top level it contains an optional ruleSetName, an optional version, and a list of rules.

{
  "version": "1.0",
  "ruleSetName": "Campaign",
  "rules": [
    {
      "cases": [
        {
          "condition": {},
          "then": [
            { "target": {}, "actions": [] }
          ]
        }
      ],
      "default": {
        "then": [ { "target": {}, "actions": [] } ]
      }
    }
  ]
}

How a rule is evaluated, per record

Multiple rules: you can list several rules. Every rule is evaluated for every record, in order. Rules are cumulative: more than one can fire for the same record. Use cases/default when you want a single "pick one" outcome; use separate rules for independent effects that can happen together. If two rules change the same property of the same target, the last one wins.

Each entry in a then list is a clause: one target plus an ordered list of actions to run on it.

Targets — what a rule acts on

A target selects the object(s) an action runs on. It always resolves within the current record's page(s).

type
Selects by
Acts on
layer
Layer name
The items on that layer
xml_tag
XML tag name
A frame or a tagged text range
data_merge_tag
Placeholder name
A Data Merge placeholder (image, QR, or text)
{ "type": "layer", "name": "premium_offer" }

Referencing a Data Merge placeholder by kind

A data_merge_tag target chooses which kind of placeholder to act on using a name prefix:

Prefix
Kind
Example
@
Image placeholder
"@hero" → the image placeholder named hero
#
QR-code placeholder
"#ticket" → the QR placeholder named ticket
(none)
Text placeholder
"title" → the text placeholder named title
{ "type": "data_merge_tag", "name": "@hero" }

Actions — what a rule does

Each clause runs an ordered list of actions on its target. Actions apply top-to-bottom; a later action overrides an earlier one on the same target.

Action name
Parameters
Effect
show
—
Make the target visible
hide
—
Make the target hidden
replace_text
value (string)
Replace the text content
set_character_style
styleName (string)
Apply a named character style
set_paragraph_style
styleName (string)
Apply a named paragraph style
set_fill_color
color (hex #RRGGBB or swatch name)
Change the fill color
set_stroke_color
color (hex #RRGGBB or swatch name)
Change the stroke color
resize_frame
width, height
Resize the frame
move_frame
x, y
Move the frame's top-left origin
{ "name": "set_fill_color", "color": "#D4AF37" }

Notes

Which actions work on which targets

Action \ Target
layer
xml_tag
data_merge_tag
show / hide
✓
✓
✓
replace_text
Not supported
✓
✓
set_character_style
Not supported
✓
✓
set_paragraph_style
Not supported
✓
✓
set_fill_color
Not supported
✓
✓
set_stroke_color
Not supported
✓
✓
resize_frame
Not supported
✓
✓
move_frame
Not supported
✓
✓

Using an action on a target that isn't marked ✓ is rejected when your rules file is checked (see Errors & Warnings).

Conditions — deciding when a rule applies

A condition is a small JSON structure, never a text expression to be parsed, so column names with spaces or punctuation (e.g. Life Time Spend) are written verbatim with no escaping.

There are three building blocks:

Comparison

{
  "column": "Life Time Spend",
  "operator": ">",
  "value": 10000,
  "valueColumn": "cost",
  "type": "number"
}

Operators

Group
Operators
Notes
Comparison
==, !=, >, >=, <, <=
>, >=, <, <= are for numbers (or text, alphabetically), not booleans
Set membership
in, not_in
value must be a non-empty array; valueColumn not allowed
Text matching
contains, starts_with, ends_with
String only, case-sensitive

Right-hand side: provide exactly one of:

Value types & how comparisons are made

Group: all (AND) and any (OR)

{ "all": [] }
{ "any": [] }

Groups can nest to any depth, and their children can be comparisons, other groups, or not blocks.

Not (flipping condition)

Wrap any single condition to negate it:

{ "not": { "column": "sku", "operator": "contains", "value": "PROMO" } }

This reads as "SKU does not contain PROMO." (For simple "is not one of a set" tests, the not_in operator is usually simpler.)

Condition examples

tier is VIP:

{ "column": "tier", "operator": "==", "value": "VIP" }

Life Time Spend greater than 10000:

{ "column": "Life Time Spend", "operator": ">", "value": 10000, "type": "number" }

Compare Life Time Spend to cost:

{ "column": "Life Time Spend", "operator": ">", "valueColumn": "cost", "type": "number" }

tier is VIP AND spend over 10000:

{
  "all": [
    { "column": "tier", "operator": "==", "value": "VIP" },
    { "column": "spend", "operator": ">", "value": 10000, "type": "number" }
  ]
}

region is not EU or UK:

{ "column": "region", "operator": "not_in", "value": ["EU", "UK"] }

A nested example combining VIP status, US or UK region, and not blocked:

{
  "all": [
    { "column": "tier", "operator": "==", "value": "VIP" },
    { "any": [
      { "column": "country", "operator": "==", "value": "US" },
      { "column": "country", "operator": "==", "value": "UK" }
    ] },
    { "not": { "column": "status", "operator": "==", "value": "Blocked" } }
  ]
}

Worked examples

Example 1: Show a VIP badge only for VIP records

{
  "ruleSetName": "VIP Badge Display",
  "rules": [
    {
      "cases": [
        {
          "condition": { "column": "tier", "operator": "==", "value": "VIP" },
          "then": [
            { "target": { "type": "data_merge_tag", "name": "vip_badge" },
              "actions": [ { "name": "show" } ] }
          ]
        }
      ],
      "default": {
        "then": [
          { "target": { "type": "data_merge_tag", "name": "vip_badge" },
            "actions": [ { "name": "hide" } ] }
        ]
      }
    }
  ]
}

VIP records show the badge; everyone else hides it.

Example 2: Color a badge by tier (first match wins)

{
  "ruleSetName": "Badge tier",
  "rules": [
    {
      "cases": [
        { "condition": { "column": "tier", "operator": "==", "value": "VIP" },
          "then": [ { "target": { "type": "data_merge_tag", "name": "badge" },
                      "actions": [ { "name": "set_fill_color", "color": "#D4AF37" } ] } ] },

        { "condition": { "column": "spend", "operator": ">", "value": 10000, "type": "number" },
          "then": [ { "target": { "type": "data_merge_tag", "name": "badge" },
                      "actions": [ { "name": "set_fill_color", "color": "#C0C0C0" } ] } ] },

        { "condition": { "column": "spend", "operator": ">", "value": 1000, "type": "number" },
          "then": [ { "target": { "type": "data_merge_tag", "name": "badge" },
                      "actions": [ { "name": "set_fill_color", "color": "#CD7F32" } ] } ] }
      ],
      "default": {
        "then": [ { "target": { "type": "data_merge_tag", "name": "badge" },
                    "actions": [ { "name": "hide" } ] } ]
      }
    }
  ]
}

Reads as: VIP → gold; else if spend > 10,000 → silver; else if spend > 1,000 → bronze; otherwise hide the badge. Cases are tested top-to-bottom and only the first match applies.

Example 3: Reveal a promo layer for high-spend records

{
  "ruleSetName": "Premium Offer",
  "rules": [
    {
      "cases": [
        {
          "condition": { "column": "lifetime_spend", "operator": ">", "value": 10000, "type": "number" },
          "then": [
            { "target": { "type": "layer", "name": "premium_offer" },
              "actions": [ { "name": "show" } ] }
          ]
        }
      ]
    }
  ]
}

This assumes the premium_offer layer is hidden in the template. It stays hidden by default and is shown only for records that match. If the layer is visible in the template, add a default case with a hide action to hide it for records that don't match.

Example 4: Swap disclaimer text by region using an xml_tag

{
  "ruleSetName": "Regional Disclaimer",
  "rules": [
    {
      "cases": [
        {
          "condition": { "column": "region", "operator": "==", "value": "EU" },
          "then": [
            { "target": { "type": "xml_tag", "name": "disclaimer" },
              "actions": [
                { "name": "replace_text", "value": "This offer is subject to EU consumer protection regulations." },
                { "name": "set_character_style", "styleName": "Legal-EU" }
              ] }
          ]
        },
        {
          "condition": { "column": "region", "operator": "==", "value": "US" },
          "then": [
            { "target": { "type": "xml_tag", "name": "disclaimer" },
              "actions": [
                { "name": "replace_text", "value": "Void where prohibited. See terms for details." },
                { "name": "set_character_style", "styleName": "Legal-US" }
              ] }
          ]
        }
      ],
      "default": {
        "then": [
          { "target": { "type": "xml_tag", "name": "disclaimer" },
            "actions": [ { "name": "replace_text", "value": "Terms and conditions apply." } ] }
        ]
      }
    }
  ]
}

This targets the text range tagged disclaimer in the template. EU records get EU-specific wording and legal styling, US records get US-specific wording and styling, and every other region falls back to the generic disclaimer text in the default case.

Constraints

Errors & Warnings

The feature distinguishes two kinds of problems:

Problems that stop the job (checked up front, before any output is produced). These are things that are wrong with the job setup or the rules file itself, so the whole job is rejected with a clear message and nothing is produced. Examples:

Fixing these means correcting the rules file (or the job configuration) and resubmitting.

Problems that are reported but don't stop the job (per record). These are situations the engine can safely work around, so the batch continues and the issues are collected in the job's warnings:

Review the warnings after a run to catch typos in names or unexpected data.

Copyfitting

The Data Merge API can automatically resize text that overflows its frame after a merge. When a merged record produces overset text, copyfitting reduces the font size, within limits that you define, so that the text fits. Copyfitting runs only when you enable it and only when the merged document has overset present.

Example use cases

Copyfitting is most useful when the length of merged content varies and cannot be predicted at design time:

How it works

Add a copyfittingSettings object inside params. It accepts the following fields:

Copyfitting is applied after the merge, before export, once per output document. Threaded text across multiple frames is treated as a single story.

Note: Missing template fonts are automatically replaced with the default font, and output generation continues normally. However, copyfitting calculations are then based on the substitute font, not the original, so the output may not appear as expected.

Example request

Enable copyfitting by adding copyfittingSettings to params:

curl --location --request POST https://indesign.adobe.io/v4/merge-data \
--header "Authorization: Bearer {YOUR_OAUTH_TOKEN}" \
--header "x-api-key: {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--data-raw '{
"assets": [
   {
      "source": {
        "url": "{PRE-SIGNED_URL}",
        "storageType": "Azure"
      },
      "destination": "dataMergeTemplate.indd"
    },
    {
      "source": {
        "url": "{PRE-SIGNED_URL}",
        "storageType": "Azure"
      },
      "destination": "FileNames.csv"
    }
],
"params": {
 "targetDocument": "dataMergeTemplate.indd",
 "outputMediaType": "application/pdf",
 "outputFolderPath":  {OUTPUT_FOLDER_PATH},
 "outputFileBaseString": "merged",
 "dataSource": "FileNames.csv",
 "recordRange": "All",
 "copyfittingSettings": {
    "enabled": true,
    "unresolvedOversetHandling": "warn",
    "multiFrameSyncScope": "paragraph_style_and_page",
    "scope": {
      "global": {
        "constraints": {
          "fontSize": { "minPercent": 80}
        }
      }
    }
  }
}
}'

To enable copyfitting with default limits, copyfittingSettings can be as small as setting enabled to true and omitting the rest.

Multi-frame sync scopes

Multi-frame sync keeps related frames visually consistent by extending a reduction beyond just the overset frame. Only the font-size reduction by percentage is synced (not the absolute point size, and no other property). If you omit multiFrameSyncScope, sync is skipped and only the overset frames change. When present, it must be one of the following.

Conflict rule: when frames in a group would need different reductions, the largest (most aggressive) reduction wins for the group, subject to the global minimum: no text shrinks below minPercent of its original size.

Unresolved overset handling

If text is still overset after copyfitting has reduced text to minPercent of its original size, unresolvedOversetHandling controls the outcome:

Copyfitting in the response

Copyfitting adds no new top-level response field. Results appear per record, inside the existing warnings object.

Still overset with "warn":

"warnings": {
 "oversetText": [
   { "page": "2",
     "charactersInOverset": "142",
     "wordsInOverset": "24",
     "oversetText": "...overflowing text..."
    }
 ]
}

Records omitted with "omit_records":

"warnings": { "omittedDataRecordNumbers": "5,6" }

If copyfitting hits a script or engine error, the whole job fails (no output for that job):

{
"status": "FAILURE",
"errorCode": "capability_error",
"errorString": "Capability error: Copyfitting failed. <detail>"
}

Pre-signed URL Support in CSV

You can provide pre-signed URLs for image assets directly in the CSV file, in addition to the input request. In the Image column (prefixed with "@"), a pre-signed URL of the image asset can be provided for each row; the images will be fetched from the URL and made available for use. Only existing DAM assets are supported.

How it works

Example of input CSV having an Image column value (prefixed with ‘@’) containing a pre-signed URL Records

Example of input payload

curl --location --request POST 'https://indesign.adobe.io/v4/merge-data' \
--header 'Authorization: Bearer {YOUR_OAUTH_TOKEN}' \
--header 'x-api-key: {YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data-raw '{
 "assets": [
   {
     "source": {
       "url": "{PRE-SIGNED_URL}",
       "storageType": "Azure"
     },
     "destination": "dataMergeTemplate.indd"
   },
   {
     "source": {
       "url": "{PRE-SIGNED_URL}",
       "storageType": "Azure"
     },
     "destination": "Directory_Names.csv"
   }
 ],
 "params": {
   "targetDocument": "dataMergeTemplate.indd",
   "outputMediaType": "application/x-indesign",
   "outputFolderPath": {OUTPUT_FOLDER_PATH},
   "outputFileBaseString": "merged",
   "dataSource": "Directory_Names.csv",
   "presignedUrlsInDataSource": true
 },
 "outputs": [
   {
     "destination": {
       "url": "{PUT-SIGNED_URL}"
     },
     "source": "{OUTPUT_FOLDER_PATH}/merged-1.pdf"
   }
 ]
}'

Folder Structure and Zipped Output

The Data Merge API supports organizing your outputs in a structured folder hierarchy, and a zipped folder containing all the outputs that have been created from the successful data merge.

How it works

Note:

Folder Name Constraints

Certain special characters or words are not supported by the platform and are automatically normalized.

Here is a sample CSV file demonstrating a column header with prefix ‘~’. Records

Example of input payload which is same as it is currently.

curl --location --request POST 'https://indesign.adobe.io/v4/merge-data' \
--header 'Authorization: Bearer {YOUR_OAUTH_TOKEN}' \
--header 'x-api-key: {YOUR_API_KEY}' \
--header 'Content-Type: application/json' \
--data-raw '{
  "assets": [
    {
      "source": {
        "url": "{PRE-SIGNED_URL}",
        "storageType": "Azure"
      },
      "destination": "dataMergeTemplate.indd"
    },
    {
      "source": {
        "url": "{PRE-SIGNED_URL}",
        "storageType": "Azure"
      },
      "destination": "FolderNames.csv"
    }
  ],
  "params": {
    "targetDocument": "dataMergeTemplate.indd",
    "outputMediaType": "image/png",
    "outputFolderPath": {OUTPUT_FOLDER_PATH},
    "outputFileBaseString": "merged",
    "dataSource": "FolderNames.csv",
    "recordRange": "All",
    "outputCompressMediaType": "application/zip",
    "outputCompressName": "Works only if outputCompressMediaType is provided",
    "outputFolderBaseString": "Merged folder name; multi-record outputs and empty ~ values go here. Default: mergedFiles",
    "hyphenationSettings": {
      "afterFirst": 3,
      "beforeLast": 3,
      "wordsLongerThan": 6,
      "ladderLimit": 2,
      "zone": 0.15,
      "capitalizedWords": false,
      "lastWord": true,
      "acrossColumns": false,
      "dictionarySettings": [
        {
          "language": "English: USA",
          "wordList": ["~word1", "ex~word2"]
        },
        {
          "language": "English: UK",
          "wordList": ["~word3", "~word4"]
        }
      ]
    }
  },
  "outputs": [
    {
      "destination": {
        "url": "{PUT-SIGNED_URL}"
      },
      "source": "{OUTPUT_FOLDER_PATH}/{outputs}"
    }
  ]
}'

When folder structuring is not supported, and output files will move to ‘outputFolderBaseString’

outputMediaType
params
JPEG (image/jpeg)
allowMultipleRecordsPerPage set to true
PNG (image/png)
allowMultipleRecordsPerPage set to true
PDF (application/pdf)
recordsPerFile ≠ 1 and recordRange = "All"
recordsPerFile ≠ 1 and recordRange = "1-2, 5"
allowMultipleRecordsPerPage set to true
InDesign (application/x-indesign)
recordsPerFile ≠ 1 and recordRange = "All"
recordsPerFile ≠ 1 and recordRange = "1-2, 5"
allowMultipleRecordsPerPage set to true

Why: Folder structuring via the ~ column applies only when the merge job has a clear one-to-one mapping between each data row and its output. In that case, each row’s ~ value defines the folder hierarchy for that record’s output file. If allowMultipleRecordsPerPage is true, multiple records can share a page, so per-row folder paths are ambiguous. If recordsPerFile is not 1, records are combined into multi-page output documents, so many rows contribute to the same file and per-row folder paths are no longer meaningful. In these cases, the API ignores the ~ column and saves output files to the folder specified by outputFolderBaseString.

Parameter Name Change in Advanced Data Merge API

The pagesPerDocument parameter from the base Data Merge API has been renamed to recordsPerFile in the Advanced Data Merge API. Only the name has changed. The parameter still controls how many records are merged into each output document or batch.

Output Path Variations in Advanced Data Merge API

This section explains where the Advanced Data Merge API writes its output files, and how that location and filename change depending on whether you provide the outputFolderPath and outputFileBaseString parameters in the request. There are four possible combinations of these two parameters; the table below summarizes all four before the detailed cases and examples that follow.

outputFileBaseString omitted
outputFileBaseString provided
outputFolderPath omitted
Case 1: temporary folder, filename from source document
Case 2: temporary folder, filename from base string
outputFolderPath provided
Case 3: outputFolderPath, filename from source document
Case 4: outputFolderPath, filename from base string

Unlike the base Data Merge API, the Advanced Data Merge API writes all output files for a job directly to the output folder (temporary or outputFolderPath). It doesn't create range1, range2, or other batch subfolders. If two files have the same name, the API adds a numbered suffix such as (1) to the filename.

Comparing v3 and v4 output structure

The following example shows the output of the same job in each API version. The job sets outputFolderPath to "ResultFolder" and outputFileBaseString to "MergedOutput", and the output is split into two batches.

Base API (v3):

Advanced Data Merge API (v4):

In v3, each batch is saved to its own rangeN folder with the same filename. In v4, all batches are saved to one folder, and each additional batch gets a numbered suffix, such as (1).

Case 1: Both Parameters Missing

When neither outputFolderPath nor outputFileBaseString is provided:

Example:

Case 2: Only outputFileBaseString Provided

When only outputFileBaseString is specified:

Example:

Case 3: Only outputFolderPath Provided

When only outputFolderPath is specified:

Example:

Case 4: Both Parameters Provided

When both parameters are specified:

Example:

Note: