DemandFlow Support Centre

DemandFlow REST API: Full Developer Reference

ReferenceAPI Reference31/08/2026Updated 31/08/2026
Single-page reference for the whole REST API: base URL and tokens, comboKeys, error handling, every endpoint, the streaming contract, and the mistakes that most often catch out a new integration.

Overview

This is the single-page reference for the DemandFlow REST API. It covers every endpoint, how records are addressed, how streaming responses behave, and the mistakes that most often catch out a new integration. Each endpoint also has its own article if you want the detail on one call in isolation.

Base URL and tokens

Every call goes to the same base URL:

https://rest.demandflow.com

Authentication is a Personal Access Token (PAT) sent as a bearer token on every request:

Authorization: Bearer <your-pat>

Create one from your avatar, then user profile, then the PAT Tokens tab. The value is shown once, so copy it immediately. A token inherits the permissions of the user who created it, is bound to that user's subscription, and can be revoked or deleted at any time from the same panel.

Treat a token like a password. Load it from an environment variable or a secrets manager rather than source control, and use a separate token per integration so that one leak can be revoked without breaking everything else.

First request

curl -H "Authorization: Bearer $TOKEN" \
  "https://rest.demandflow.com/v1/entities/PPL/SUB"

That streams back every person in your tenant.

Endpoints at a glance

MethodPathPurpose
GET/v1/objects/{id}Read one record by id
POST/v1/objectsCreate a record
PATCH/v1/objects/{id}Update selected fields
DELETE/v1/objects/{id}Delete a record
GET/v1/entities/{entity}/{comboKey}Stream a list by entity and comboKey prefix
POST/v1/queryRun several list queries in parallel in one round trip
POST/v1/files/publicPresigned upload for public assets

comboKeys, how records are addressed

Every record carries a comboKey that encodes its full ancestry as pipe-separated ABBREV:id segments, starting at your subscription:

"comboKey": "SUB:13c16ba0-f7ab-...|ENT:9f3...|BU:41c...|PJ:7ab...|ID:c92..."

List queries match on a prefix of that key, anchored at the left and matched whole segments at a time. The prefix is the filter. "Every application under patent xyz789" is the entity PATENTA plus a prefix ending |PAT:xyz789. There is no "contains" match, and no way to query on a middle segment alone.

  • The SUB: value is your subscription id. It appears on the comboKey of every record you read back. It is never a token or a key, and no credential of any kind belongs inside a query.
  • SUB on its own is a valid prefix, meaning every record of that entity in your tenant.
  • Some entities carry secondary hierarchies on comboKey2 and comboKey3. POST /v1/query lets you choose which one to match on.
  • When creating a record, the comboKey you send must end with |ENT:. The new id is appended directly onto that suffix.
  • In a URL path, encode | as %7C if your client does not do it for you.

Errors

Single-record endpoints return a JSON body with error and message fields, and a standard HTTP status:

StatusMeaning
400Malformed request, such as a bad JSON body or a missing id in the path
401No Authorization header
403Invalid or expired token, or the record belongs to another tenant
404Record not found
405Method not allowed on that path
5xxServer-side failure. Retry after a short delay

Streaming endpoints behave differently. Once a list response has started, the status has already been sent, so failures arrive inside the body as a line with _type: "error" and a message. A failure in one query line of a /v1/query batch does not stop the others. Check every line, not just the status.

Working with single records

GET /v1/objects/{id}

Reads one record by id, scoped to your subscription. The optional entity query parameter (uppercase entity code) lets the API route the read directly without an extra lookup, and is safe to omit in almost all cases.

Returns 200 with every field on the record, or 404 if the id does not exist or belongs to another tenant.

curl -H "Authorization: Bearer $TOKEN" \
  "https://rest.demandflow.com/v1/objects/a1b2c3d4-e5f6-4890-abcd-ef1234567890"
{
  "id": "a1b2c3d4-e5f6-4890-abcd-ef1234567890",
  "entity": "PPL",
  "level": 220,
  "comboKey": "SUB:...|ENT:a8da263a-...",
  "name": "Ada Lovelace",
  "_df_version": 4,
  "created": 1776336000000,
  "updated": 1776338516765
}

POST /v1/objects

Creates a record and returns it with its new id and version. Three system fields are required on every create:

FieldTypeNotes
entitystringRequired. Uppercase entity code, for example PPL or PATENT
levelnumberRequired. The numeric level from that entity's definition
comboKeystringRequired. Must end with |ENT:

Everything else is entity-specific. Returns 201 with the created record, including the server-assigned id and _df_version: 0, or 400 if the body is missing, is not a JSON object, or is invalid JSON.

curl -X POST "https://rest.demandflow.com/v1/objects" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "entity":   "PATENTA",
    "level":    430,
    "comboKey": "SUB:<your-sub-id>|PAT:<parent-patent-id>|ENT:",
    "name":     "Application Title"
  }'

Do not drop the trailing |ENT:. Without it, the new id is appended to whatever segment came last and the record ends up with a malformed comboKey. It will then not be found by any prefix query, including its own parent's.

PATCH /v1/objects/{id}

Updates selected fields and returns the full updated record. Send only the fields you want to change, since anything you omit is left untouched. The system fields id, ownerId and created are ignored if present.

curl -X PATCH "https://rest.demandflow.com/v1/objects/a1b2c3d4-..." \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Ada Lovelace (Countess)", "title": "Countess"}'

A patch is not a bare write. On the way through, the platform stamps updated and updatedId, increments _df_version, pushes the change live to anyone with the record open, fires any automations configured on that entity, and writes an audit entry.

Because each call runs that whole pipeline, batch your changes. One patch carrying twenty fields costs a fraction of twenty single-field patches to the same record. Use the returned record rather than assuming your submitted values landed verbatim.

DELETE /v1/objects/{id}

Deletes a record and returns 204 with an empty body. The record is removed, connected clients are updated live, and the deletion is written to the audit log. Returns 403 if the record belongs to another tenant, or 404 if it does not exist.

Deletion is immediate and cannot be reversed through the API. Deleting a parent does not delete its children, which keep their comboKeys and are left orphaned. For anything high-risk, prefer archiving, by setting a status field, over deleting.

Querying lists

There are two shapes. One entity and one prefix goes in the URL. Anything more, such as several entities at once, counts, text filters or time windows, goes to /v1/query.

GET /v1/entities/{entity}/{comboKey}

ParameterInNotes
entitypathRequired. Uppercase entity code
comboKeypathRequired. Prefix to match. SUB alone lists every record of that entity in your tenant
fieldsqueryOptional. Comma-separated field names to return

The response is a JSON array, streamed as records are read, and each item carries a _type: "data" marker. Small results can be read whole and parsed normally.

# every person in the tenant
curl -H "Authorization: Bearer $TOKEN" \
  "https://rest.demandflow.com/v1/entities/PPL/SUB"

# applications under one patent, three fields only
curl -H "Authorization: Bearer $TOKEN" \
  "https://rest.demandflow.com/v1/entities/PATENTA/SUB:abc123%7CPAT:xyz789?fields=id,name,status"

POST /v1/query

Runs several list queries in parallel and streams the combined result as NDJSON. The body is a raw JSON array of query lines, with no wrapper object. Each line runs independently and writes its records into one shared stream, which is how you load everything behind a dashboard in a single round trip.

Query line fieldTypeNotes
entitystringRequired. Uppercase entity code
comboKeystringRequired. Name of the key attribute to match on. For standard queries this is literally "comboKey". Use "comboKey2" or "comboKey3" for an entity's secondary hierarchies
querystringRequired. The prefix to match on that key, usually starting SUB:{your-sub-id}
loadstringComma-separated fields to project. Omit for the whole record
limitnumberHard cap on items returned for this line
countOnlybooleanEmits a single count record instead of the data
filterobjectCase-insensitive substring filter, { field, term }. Use field: "*" to match anywhere in the record
tsStart, tsEndnumberLOG entity only. Epoch-ms range, used with a timestamp-prefixed comboKey
taganyEchoed back on this line's meta records, so counts and scan stats can be matched to the line that produced them

Parallel load:

curl -X POST "https://rest.demandflow.com/v1/query" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '[
    {"entity":"PPL",    "comboKey":"comboKey", "query":"SUB", "load":"id,name"},
    {"entity":"ACTION", "comboKey":"comboKey", "query":"SUB", "load":"id,name,status"}
  ]'

Text search across a log window:

[
  {
    "entity":   "LOG",
    "comboKey": "comboKey",
    "query":    "SUB:<sub>|LOGSOURCE:<source>|TS:",
    "tsStart":  1776000000000,
    "tsEnd":    1776600000000,
    "filter":   { "field": "*", "term": "timeout" },
    "limit":    500,
    "tag":      "timeouts-last-week"
  }
]

What comes back

LineHow to tellWhat it means
RecordHas an id and no _typeYour data. Records from different query lines are interleaved, so route on each record's entity
Count{ _meta: true, _type: "count", entity, count, tag? }The result of a countOnly line
Scan{ _meta: true, _type: "scan", entity, scanned, matched, tag? }Emitted when a text filter ran, so you can see how much was read against how much matched
Error{ _type: "error", message, entity, source }A failure on one query line. The rest of the batch keeps streaming

Consuming a stream

Two rules cover every client. Split on newlines with a carry-over buffer, and check _type before you trust a line.

const res = await fetch('https://rest.demandflow.com/v1/query', {
  method: 'POST',
  headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' },
  body: JSON.stringify(queries)
});

const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';

while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });

  const lines = buffer.split('\n');
  buffer = lines.pop();                     // last line may be incomplete

  for (const line of lines) {
    if (!line.trim()) continue;
    const row = JSON.parse(line);

    if (row._type === 'error') { handleError(row.message); continue; }
    if (row._meta) { handleMeta(row); continue; }      // count / scan totals
    handleRecord(row);
  }
}
if (buffer.trim()) handleRecord(JSON.parse(buffer));   // no trailing newline

Never store a trimmed record as if it were whole. Anything returned under load or fields is a partial. Keep partials out of any cache that your full-record reads share, or a later lookup will return a record with most of its fields missing.

Public file uploads

POST /v1/files/public

A two-step presigned upload for assets that must be reachable without a login. The API never sees the file bytes. Post { "filename": "network-diagram.png" } and you get back a form to upload with:

Response fieldUse
urlWhere to POST the file bytes in step 2
fieldsOpaque form fields from S3. Submit every one of them, before the file itself
fileKeyThe object's key within the bucket
fileUrlThe final public URL. Store this on any record that references the file, never the presigned url

Maximum 10 MB per upload, and the presigned form is valid for 10 minutes. The filename field is optional. Characters other than letters, digits, dot, underscore and hyphen become underscores, and the name is truncated to 120 characters.

Public means public. Files land in your tenant's public bucket and are readable by anyone with the URL, with no authentication. Use it for knowledge base images, portal diagrams and logos. Never use it for contracts, customer data, invoices, or anything else you would not print on a billboard. Private uploads have their own endpoints inside the app.

Limits and gotchas

Reading the stream

  • Split on newlines and keep the tail in a buffer, because chunks cut lines in half.
  • Check _type before id on every line.
  • An error line does not end the stream. Keep reading.
  • Ordering across a /v1/query batch is not defined. Route on entity, comboKey or tag.

Cost and volume

  • load and fields save bandwidth, not query cost. The record is read in full, then trimmed.
  • filter runs after the read, not as part of it. The scan record tells you what it cost.
  • filter: { field: "*" } inspects the whole record. That is fine for a few thousand rows, not for a whole-entity sweep.
  • A bare SUB prefix on a large entity really will stream everything. Pass limit unless you mean it.

Tenancy

  • Your tenant is resolved from the token, never from the request. There is no cross-tenant read, and no parameter that could make one.
  • A token can do exactly what its owner can do, and no more.
  • Start prefixes at SUB, or at SUB:<your-sub-id> when drilling into a hierarchy.

Sharp edges

  • A create comboKey must end with |ENT:. This is the most common integration bug.
  • /v1/entities/... returns a JSON array, while /v1/query returns NDJSON. They need different parsers.
  • Encode | as %7C in URL paths.
  • Deleting a parent orphans its children. Enumerate before you delete.

See also

  • DemandFlow REST API Overview
  • Getting an API Token (PAT)
  • The per-endpoint articles in this category, for detail on a single call
apirestreferencepattokencombokeyndjsonstreamingqueryobjectsentitiesfiles

Was this article helpful?

★★★★★
← Back to Knowledge Base