> ## Documentation Index
> Fetch the complete documentation index at: https://www.composer.id/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart

> Mint a sandbox key and publish your first workforce decision into a destination in five API calls. Nothing to install.

This page goes from nothing to a published decision and its receipt using only `curl` against the hosted sandbox. Every call below is a documented `/v1` route; the [API reference](/api-reference/overview) has the full request and response shapes and a playground that runs them for you.

<Steps>
  <Step title="Mint sandbox credentials">
    There is no sign-up. One call gives you a tenant of your own with a personal API key, OAuth client credentials and a webhook signing key.

    ```bash Mint credentials theme={"system"}
    curl -s -X POST https://sandbox.composer.id/v1/sandbox/keys \
      -H 'Content-Type: application/json' \
      -d '{"label": "my-first-key"}'
    ```

    ```json Response (abridged) theme={"system"}
    {
      "tenant": "tn_RjH3CJm8SlH6",
      "api_key": "sk_sandbox_...",
      "client_id": "cid_...",
      "client_secret": "cs_...",
      "webhook_signing_key": "whsec_...",
      "expires_at": "2026-10-22T11:26:29Z",
      "token_endpoint": "/v1/oauth/token"
    }
    ```

    Save the `api_key`; the secrets are shown once. Everything you create is visible only to credentials from this tenant.

    ```bash Keep it in your shell theme={"system"}
    export KEY=sk_sandbox_...
    export BASE=https://sandbox.composer.id/v1
    ```
  </Step>

  <Step title="Check you are in">
    ```bash The index theme={"system"}
    curl -s $BASE -H "Authorization: Bearer $KEY"
    ```

    The index lists every route and reports `"durable": true`, which means your intents survive between calls. A missing or wrong key is a `401`.
  </Step>

  <Step title="Create an intent">
    An intent is the decision Triage made, as a record. The payload is yours; composerID mints the `intent_id`.

    ```bash Create theme={"system"}
    curl -s -X POST $BASE/intent \
      -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
      -d '{
        "payload": {
          "channel": "contingent_hire",
          "role_title": "Data Engineer",
          "start_date": "2026-11-02",
          "location": {"country": "gb"}
        }
      }'
    ```

    The `201` response is the intent record: an `intent_id` beginning `itn_`, `intent_version: 1`, and your payload. Keep the id.

    ```bash Keep it in your shell theme={"system"}
    export ID=itn_...
    ```
  </Step>

  <Step title="Plan and preflight">
    `/plan` tells you which destinations this intent would publish to. `/preflight` checks it against one destination's required fields before anything is sent.

    <CodeGroup>
      ```bash Plan theme={"system"}
      curl -s -X POST $BASE/plan \
        -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
        -d "{\"intent_id\": \"$ID\"}"
      ```

      ```bash Preflight against Fieldglass theme={"system"}
      curl -s -X POST $BASE/preflight \
        -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
        -d "{\"intent_id\": \"$ID\", \"platform\": \"fieldglass\"}"
      ```
    </CodeGroup>

    A `200` means the destination has everything it needs. A `422` means it does not, and the body carries an `enrichment_request` naming the `missing_fields`. Answer them and preflight again:

    ```bash Answer the missing fields theme={"system"}
    curl -s -X POST $BASE/enrichment-request \
      -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
      -d "{\"intent_id\": \"$ID\", \"answers\": {\"cost_center\": \"CC-7\", \"worker_type\": \"CONTINGENT\"}}"
    ```

    Enrichment completes the same intent version; it does not create a new one.
  </Step>

  <Step title="See what would be written">
    Ask composerID for the exact body it would send, without sending it:

    ```bash Dry run theme={"system"}
    curl -s -X POST $BASE/publish \
      -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
      -d "{\"intent_id\": \"$ID\", \"platform\": \"fieldglass\", \"dry_run\": true}"
    ```

    ```json Preview (abridged) theme={"system"}
    {
      "mode": "create",
      "object_type": "job_posting",
      "profile": "fieldglass.contingent_requisition.v1",
      "carrier_field": "customFields.intentId",
      "idempotency_key": "itn_...-1-fieldglass-job_posting",
      "payload": {
        "jobPostingTitle": "Data Engineer",
        "startDate": "2026-11-02",
        "workLocationCountry": "GB",
        "costCenterCode": "CC-7",
        "workerTypeCode": "CONTINGENT",
        "customFields.intentId": "itn_..."
      },
      "dry_run": true
    }
    ```

    That is the record as the destination's API would receive it: its own field names, with the Intent ID in the field that carries it. Nothing was stored and no event was recorded. Every destination answers a dry run, so this is how you see what composerID would post to any of them.
  </Step>

  <Step title="Publish, then publish again">
    ```bash Publish theme={"system"}
    curl -s -X POST $BASE/publish \
      -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
      -d "{\"intent_id\": \"$ID\", \"platform\": \"fieldglass\"}"
    ```

    ```json Receipt (abridged) theme={"system"}
    {
      "target_system": "fieldglass",
      "object_type": "job_posting",
      "external_id": "FI-...",
      "idempotency_key": "itn_...-1-fieldglass-job_posting",
      "status": "published",
      "replayed": false,
      "payload": {"jobPostingTitle": "Data Engineer", "...": "..."}
    }
    ```

    The receipt carries the destination's own record id and the payload that was written: the same body the dry run showed. Run the same publish a second time. You get the same receipt with `"replayed": true` and no second record. The idempotency key is built from the intent id, its version and the destination, so a retry can never create a duplicate. Change the intent with `PATCH /intent/{id}` and the version increments; publishing that is new, not a replay.
  </Step>

  <Step title="Read the timeline">
    ```bash Timeline theme={"system"}
    curl -s $BASE/intent/$ID/timeline -H "Authorization: Bearer $KEY"
    ```

    Every event so far, in order: created, planned, preflighted, enriched, published. Destination webhooks land here too, verified against your `webhook_signing_key` and correlated to the same `intent_id`. The timeline is append-only.
  </Step>
</Steps>

<Note>
  Destinations in the sandbox are mock tenants that behave the way each vendor's published API says the real one does. The receipt you got is real in every respect except that no vendor system was written to. Each [destination page](/destinations) states the field its Intent ID lands in.
</Note>

## Run it locally

The same API runs from the repository with no dependencies beyond Python 3.11.

```bash Local sandbox theme={"system"}
git clone https://github.com/jamiegannaway/composer-id
cd composer-id
python3 -m service.server          # http://127.0.0.1:8787/v1, prints a key
```

And the conformance pack, the eighteen claims this documentation makes, can be run against any deployment, including yours:

```bash Conformance theme={"system"}
make conformance BASE=https://sandbox.composer.id/v1 KEY=$KEY
```

## Next steps

<Columns cols={2}>
  <Card title="Authentication" icon="key-round" href="/authentication">
    OAuth 2.0 client credentials, scopes, and which scope each route needs.
  </Card>

  <Card title="Connect a destination" icon="plug" href="/guides/connect">
    What a destination needs from you, and where the Intent ID lands.
  </Card>
</Columns>
