Integration API

Consuming content

Step by step – an export script that copies DDM's documents to another system and keeps it in step.

Another system can take content out of DDM: a website showing the current price lists, a shop attaching data sheets to products, a search index, an archive. All it needs is a read-only key.

1. Create a read-only key

Under Settings › API access, create a key with only Read. Choose what it sees:

  • Released content only is right for anything public. It sees what readers see in the app: nothing archived, and with Distribute only after approval nothing unapproved. It can only download files that may leave the app.
  • Everything in Documents includes drafts, for an internal archive or a migration.

Let it act as a person who sees exactly the categories the other system should receive.

2. The first full load

Fetch the categories, then page through every document and download the files:

curl https://ddm.example.com/api/ext/v1/categories -H "Authorization: Bearer $DDM_API_KEY"
curl "https://ddm.example.com/api/ext/v1/documents?limit=500" -H "Authorization: Bearer $DDM_API_KEY"
curl -o Preisliste.pdf https://ddm.example.com/api/ext/v1/documents/8d0c1f5e-…/content -H "Authorization: Bearer $DDM_API_KEY"

Follow nextCursor until it is null. Store, for each document, its id, updatedAt and currentVersion.checksum.

3. Each later run: what changed

Ask only for what changed since the newest updatedAt you have:

curl "https://ddm.example.com/api/ext/v1/documents?updatedSince=2026-10-27T03:00:00Z&limit=500" \
  -H "Authorization: Bearer $DDM_API_KEY"

For each document returned, compare currentVersion.checksum with yours. Download the file only if it differs, and update the details (name, titles, tags, categories) either way.

4. Each later run: what is gone

A deleted document is not in any list, because it no longer exists. Neither is one that was archived out of the released view, or moved where the key's person cannot see it. To find these, fetch the manifest:

curl https://ddm.example.com/api/ext/v1/sync -H "Authorization: Bearer $DDM_API_KEY"

It lists every document the key can see, in a few fields each. Delete from your system whatever you have that is not in the manifest – but only if complete is true. Above 50,000 documents the manifest is cut off, and then it cannot tell you what is gone. A worked example in four languages is in Finding deleted documents.

A complete example

A sync script in JavaScript (Node 20+). It runs as often as you like, and keeps its state in a JSON file:

import { readFile, writeFile, mkdir, rm } from 'node:fs/promises'

const BASE = 'https://ddm.example.com/api/ext/v1'
const headers = { Authorization: `Bearer ${process.env.DDM_API_KEY}` }
const STATE = './ddm-state.json'
const FILES = './files'

const state = JSON.parse(await readFile(STATE, 'utf8').catch(() => '{"since":null,"documents":{}}'))
await mkdir(FILES, { recursive: true })

async function get(path, params = {}) {
  for (let attempt = 0; ; attempt++) {
    const response = await fetch(`${BASE}${path}?${new URLSearchParams(params)}`, { headers })

    if (response.ok) return response
    if ((response.status === 429 || response.status >= 500) && attempt < 5) {
      await new Promise(resolve => setTimeout(resolve, 1000 * (Number(response.headers.get('retry-after')) || 2 ** attempt)))
      continue
    }
    throw new Error(`${response.status} ${path}: ${await response.text()}`)
  }
}

// What changed — everything, on the first run.
let cursor
do {
  const page = await (await get('/documents', {
    limit: '500',
    ...(state.since ? { updatedSince: state.since } : {}),
    ...(cursor ? { cursor } : {})
  })).json()

  for (const document of page.data) {
    const known = state.documents[document.id]

    if (document.currentVersion && known?.checksum !== document.currentVersion.checksum) {
      const file = await get(`/documents/${document.id}/content`)
      await writeFile(`${FILES}/${document.id}`, Buffer.from(await file.arrayBuffer()))
    }

    state.documents[document.id] = { name: document.name, checksum: document.currentVersion?.checksum, titles: document.titles }
    state.since = document.updatedAt
  }

  cursor = page.nextCursor
} while (cursor)

// What is gone.
const manifest = await (await get('/sync')).json()

if (manifest.complete) {
  const present = new Set(manifest.documents.map(document => document.id))

  for (const id of Object.keys(state.documents)) {
    if (!present.has(id)) {
      await rm(`${FILES}/${id}`, { force: true })
      delete state.documents[id]
    }
  }
}

await writeFile(STATE, JSON.stringify(state, null, 2))

Tips

  • Titles per market: titles holds a title per market code (GET /markets lists them). Show titles[code] ?? name.
  • Text for a search index: GET /documents/{id}/text gives the text DDM read from the file.
  • Previews: GET /documents/{id}/thumbnail gives the preview image of the first page.
  • Downloading again for nothing: send the last ETag as If-None-Match, and an unchanged file answers 304.
  • How often: every few minutes is fine. Each request counts against the key's 600 per minute.