Assembly: the request, the estimate and the job resource
POST /api/v1/dmgen/assemble/ builds a model from a tree of reused and new components; this page is the path a script or an agent takes, and sdcmodeler (see the MCP servers) is its client. Around it are two more endpoints: an estimate before any money moves, and one job to poll while the long work runs (both since 4.17.0). Authentication is a session or an API key (Authorization: Token sdcs_...; see API Keys). Prices are in credits, at 1,000 per dollar (see Wallet and Billing).
The request
{"title": "Vitals",
"description": "Bedside observations",
"data": {"label": "Vitals",
"components": [{"ct_id": "abc123abc123abc123abc123"},
{"label": "Pulse", "data_type": "XdCount", "description": "Beats per minute", "units": "beats/min"}],
"clusters": [{"label": "Context", "components": [{"label": "Note", "data_type": "XdString"}], "clusters": []}]},
"contextual": {"party": {"ct_id": "..."}},
"allow_retired": false,
"publish": true, "generate": true,
"estimate_id": "7f3a9c1e2b4d"}
| Field | |
|---|---|
title |
required, at most 255 characters; the model's title |
description |
optional; "" is the same as leaving it out |
data |
the model's root cluster: label (required, at most 110), optional ct_id to reuse a published cluster as the root, components and clusters (nested to any depth, each with the same shape) |
| a component | {"ct_id": ...} to reuse a published component, or {"label", "data_type"} to mint one, with optional description and units ("" means none). data_type is a type name such as XdString, XdCount, XdQuantity |
contextual |
optional component references for the model's audit, attestation, party, subject, provider, participation, protocol, workflow and acs, each a component reference as above |
allow_retired |
true accepts a retired component; otherwise one is refused with 409 naming its successor when it has one |
publish, generate |
see below; both default to true |
estimate_id |
the id of the estimate the person saw; see below |
The answer is 200 with the model when every component exists (mode: reuse), or 202 with a job when something must be minted (mode: mixed). The errors:
| Status | When | Body |
|---|---|---|
400 |
the tree is malformed: a component without ct_id or label and data_type, a cluster without a label |
{"error": "Invalid request", "details": {...}}; the estimate refuses it the same way |
402 |
the wallet cannot pay the whole charge | error_type: insufficient_funds, with estimated_cost_credits and balance_credits |
403 |
the key lacks a scope, or the charge would pass its limit | error_type: key_scope (with scope and the key's scopes) or key_spend_limit (with the numbers); see API Keys |
409 |
the estimate_id no longer matches the title and tree, or a retired component without allow_retired |
error_type: stale_estimate, or {"error": ...} naming the retired component |
The estimate and assemble/ are writes, so a key needs Build to call either; a key that only reads is refused with 403 key_scope.
The estimate
POST /api/v1/dmgen/assemble/estimate/ takes exactly the body assemble/ takes and builds nothing and charges nothing. It resolves every leaf the way assemble/ will: a ct_id that names a published component is reused and free; a label with a data_type is minted at 100 credits; a new cluster below the root is minted at 100 credits too. The model itself is 500 credits, and that includes its root data cluster, the model's own data cluster. The estimate's total is exactly what assemble/ charges, on either path (one rule since 4.17.5).
{"title": "Vitals",
"data": {"label": "Vitals",
"components": [{"ct_id": "abc123abc123abc123abc123"}, {"label": "Pulse", "data_type": "XdCount"}],
"clusters": []}}
answers
{"estimate_id": "7f3a9c1e2b4d",
"mode": "mixed",
"reuse": [{"path": "data.components[0]", "ct_id": "abc123abc123abc123abc123", "label": "Blood Pressure", "type": "XdQuantity"}],
"mint": [{"path": "data.components[1]", "label": "Pulse", "data_type": "XdCount"}],
"problems": [],
"model_credits": 500, "mint_credits": 100, "total_credits": 600, "total_usd": "0.60",
"wallet": {"balance_credits": 9400, "can_pay": true},
"key": {"scopes": ["build", "read", "spend"], "can_spend": true, "spend_limit_credits": 5000, "spent_credits": 0, "remaining_credits": 5000, "within_limit": true}}
problemsnames whatassemble/would refuse: act_idthat is unknown or not published, or a retired component (with its successor when there is one;allow_retired: trueaccepts it). A malformed tree never reaches the estimate: it is refused with400, as above.keyis what the calling API key may do and whether this build fits its remaining limit;nullwhen the call was not made with a key.wallet.can_payis the balance against the total.modeis the pathassemble/will take:reusewhen every component (every leaf) exists, soassemble/builds synchronously, andmixedwhen a component must be minted, so the pipeline runs and a job is returned. A new cluster alone does not make an assembly mixed: a tree whose leaves all exist but which adds a new nested cluster is built on the reuse path and still charged 100 for that cluster.
estimate_id is the first twelve hex characters of the SHA-256 of the canonical JSON of {"title": ..., "tree": ...} (keys sorted, no whitespace). sdcmodeler computes the same value locally. Hand it back to assemble/ as estimate_id: if the title or the tree changed since the estimate, assemble/ answers 409 with error_type: stale_estimate and builds nothing. An agent that shows the person an estimate and then builds with its id cannot build something else.
The publish and generate flags
assemble/ takes two booleans, publish and generate, both true by default since 4.17.2. generate needs publish.
- Every leaf existing (
mode: reuse): the response is the model, withpublished,generatedandmodel_urlbeside the catalog fields. Withpublish: falsethe clusters and the model are left in draft; withgenerate: falsethe model is published but no package is built. - Something to mint (
mode: mixed): the pipeline mints the new components and leaves everything in draft, then, when it completes, the assembly's job publishes the tree bottom-up (components, clusters, the model) and builds the package as the flags asked. The job'sresultthen carriespublished,generatedand the model's links, orfinalize_errorwith what could not be published. An upload's job publishes nothing: an upload is a draft for a person to review.
One call, one job to poll, and completed means what the caller asked for: the job reads running until finalize has published and generated (steps finalizing, publishing, generating), and only then completed (since 4.17.5). If publishing or generating fails, the job ends failed, with finalize_error in its result and as its error; the model's links are in the result either way.
What an assembly costs
One rule for the estimate and both paths (since 4.17.5):
- 500 credits for the model, including its root
datacluster. - 100 credits for every other new cluster, at any depth.
- 100 credits for every new component, at any depth.
- Reused components and clusters are free.
The reuse path charges before it builds and refunds if the build fails. The mixed path checks the whole amount (wallet and key) before the pipeline starts, and charges when the run has made the model. It never charges for more new components than the request named.
Omit description (on the model or a component) and units, or send them as ""; both mean none (since 4.17.5; before, "" was refused as blank).
The job resource
GET /api/v1/jobs/<ct_id>/ is one record for every long operation. Only its owner (or staff) can read it.
| Field | Meaning |
|---|---|
kind |
assemble (a mixed assembly), upload (a file for the pipeline), generate_package, generate_app |
status |
queued, running, completed, failed |
step |
where it is: the pipeline's own status for an assembly or an upload (parsing, parsed, agent_processing, modeling), then finalizing, publishing, generating and done; or the generator's step |
cost_credits |
what the job has charged so far, read from the ledger |
result |
on completion: dm_ct_id, model_url, catalog_url, package_url; an assembly job adds published, generated and, when something could not be published, finalize_error; an application job adds profile, app_name, app_url |
error |
why it failed |
data_source_ct_id, dm_ct_id |
what the job is about |
ct_id, url, created, updated |
the job itself |
GET /api/v1/jobs/ lists the caller's jobs, newest first (at most 100); ?kind= and ?data_source=<ct_id> narrow it.
Where jobs come from
- A mixed
assemble/answers202withjob_ct_idandjob_urlbeside thetask_idanddata_source_ct_idit always returned. The job reads the pipeline's state, so its status moves as the data source's does and finishes with the model's links. - An upload gets a job too, from the API (
POST /api/v1/uploader/api/data-sources/) or from the Upload page (since 4.17.5); find it withGET /api/v1/jobs/?data_source=<ct_id>. A job is created before its task is sent. - Package and application generation run inside the request by default, which is what the application expects. Send
{"async": true}(or?async=1) toPOST /api/v1/dmgen/dms/<ct_id>/generate/or.../generate_app/and the call answers202with a job instead; the work runs in the background and the job finishes with the links. A model's package is generated once, by someone who may publish it, as before.
Polling
Poll the job, not the task: a sensible cadence is a few seconds, backing off. completed carries the links; failed carries the reason. The agent reports a failed job to the person; nothing is charged for a pipeline that minted nothing.