POST /publish.
Three steps
1
Plan
POST /plan applies the Channel Map, the rules that say which kind of work goes to which destination, and returns the destinations this intent will be published to.The request names the intent_id; channel defaults to the channel on the intent’s payload, and platform overrides the Channel Map with an explicit destination. The response lists each destination with its platform, object_type, carrier_field (where the Intent ID is written), mode (stamp or link) and events_posture (push, poll or mixed), plus a deterministic rationale for the choice. Scope: intents:write.2
Preflight
POST /preflight checks the intent against one destination’s own requirements: the vendor’s create schema and the tenant’s own mandatory fields, picklists and custom fields.A clean pass returns 200 with ok: true, the profile validated against and an empty missing_fields. A gap returns 422 with an enrichment_request naming the missing canonical fields, the target_system, the profile that flagged them and, per field, an optional target_field and hint. The question reaches the requester before anything is written. Scope: intents:write.3
Publish
POST /publish renders the destination’s payload from the mapping profile, checks for an existing record carrying the Intent ID, creates one only if none exists, and returns 201 with a receipt.The receipt carries external_id (the destination’s own record identifier), idempotency_key, deep_link (one click to the record in the destination), status, replayed and at. Supply external_id in the request when the record already exists: composerID stamps the Intent ID onto it instead of creating a duplicate. That is required for link-mode destinations. A tenant gap found at publish time returns the same 422 as preflight. Scope: publish.Idempotency
Every publish is identified by a deterministic key:Idempotency key
object_type suffix appears only when one intent produces more than one record in a destination. A repeated publish returns the original receipt with replayed: true and touches nothing. Bumping intent_version mints a new key on purpose, so a genuine amendment publishes again while retries of the old key stay inert.
For a given decision and version there is at most one record per destination. Where a destination honours an idempotency header the adapter sends one; where it documents none, the adapter queries the destination for a record already carrying the Intent ID before creating one.
The enrichment loop
Preflight gaps become questions, not failures. Theenrichment_request is a published schema an upstream surface can render in the requester’s own words. The answers return through POST /enrichment-request as an answers object keyed by canonical field, preflight runs again, and publishing proceeds.
Enrichment completes the same decision, so it does not bump
intent_version and the publish key is unchanged. The intent is never half-published: the publish is withheld until every gap is filled.Mapping profiles
A mapping profile per destination translates the canonical intent fields into the destination’s dialect:role_title becomes an SAP Fieldglass job posting title, a Beeline requestName, a Coupa requisition line description. Preflight validates against the profile, publish renders from it, and reconciliation compares through it. Swapping a destination is a configuration change, not a rewrite.
All of this is implemented in the reference sandbox and held to the documented contract by a conformance pack that runs in continuous integration. The production service behind
api.composer.id is in build; nothing here claims it is live.Next steps
API reference
Every route, request body, response and scope.
Events and the timeline
What happens after the receipt: webhooks, reconciliation and drift.
Published schemas
EnrichmentRequest, PublishingBundle and MappingProfile at their canonical URLs.