> ## Documentation Index
> Fetch the complete documentation index at: https://docs.os.default.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Route and book through the API

> Fire a routing workflow from your server, take the scheduler it hands back, and book a meeting. Four requests end to end.

This page is for anyone calling Default from their own server, whether that is a backend service,
a chatbot, or an AI agent. You will run a workflow that decides who a lead should meet, then
book a meeting on the calendar it picks. Four requests total.

Every endpoint used here is documented in full in the [API reference](/api-reference/triggers/list-triggers).

<Note>
  Building an AI agent? The [`@defaulthq/ai-tools` package](/api/agent-tools) wraps this whole flow
  as ready made Vercel AI SDK tools.
</Note>

## Authentication

Create an API key in **Settings** → **API Keys** with the `triggers:write` and
`scheduling:write` permissions. A write key also covers reads for its surface, so these two are
enough for the whole flow. Send it on every request as `Authorization: Bearer <key>`.

<Note>
  This is a server to server API. It sends no CORS headers, so it cannot be called from a browser.
  Your API key should be kept private.
</Note>

## Before you start

You need a published workflow with an API trigger. In the workflow editor, add the API trigger,
connect a form to it, and publish. The form's fields define what your requests send. The workflow
needs a Display Scheduler step on the path you expect to book from.

## The flow

<Steps>
  <Step title="Find your trigger">
    List the API triggers in your workspace to get the trigger id and the fields its form expects.

    ```bash theme={null}
    curl https://api.default.com/v1/triggers \
      --header "Authorization: Bearer $DEFAULT_API_KEY"
    ```

    ```json theme={null}
    {
      "triggers": [
        {
          "id": "4e2c05c8-08c5-48d7-aa31-4141771f9db7",
          "name": "Inbound chat routing",
          "description": "Routes chat leads to the right rep",
          "fields": [
            { "name": "email", "label": "Email", "type": "text", "required": true, "options": null },
            {
              "name": "company_size",
              "label": "Company size",
              "type": "select",
              "required": true,
              "options": [
                { "value": "1-10", "label": "1-10" },
                { "value": "11-50", "label": "11-50" },
                { "value": "51-200", "label": "51-200" },
                { "value": "201-500", "label": "201-500" },
                { "value": "501-1000", "label": "501-1000" },
                { "value": "1000+", "label": "1000+" }
              ]
            },
            { "name": "use_case", "label": "Use case", "type": "text", "required": false, "options": null }
          ]
        }
      ]
    }
    ```

    Each field's `name` is the key you send in the next step. Fields marked `required` must have a
    value before the workflow will run. For select fields, send one of the option values. You can
    skip the email field, the next step fills it for you.

    Full reference: [`GET /v1/triggers`](/api-reference/triggers/list-triggers)
  </Step>

  <Step title="Fire the trigger">
    Send the lead's email and their responses. The `email` input is the lead's identity, and it
    automatically fills the form's email field, so you never send it twice.

    If you know web context about the lead, send it in the optional `context` object: `utmParams`,
    `gclid`, `pageUrl`, `referrer`, `userAgent`, and `ipAddress`, all optional. The workflow can
    reference it as form submission data, and it records as attribution on the booked meeting.

    ```bash theme={null}
    curl --request POST https://api.default.com/v1/triggers/4e2c05c8-08c5-48d7-aa31-4141771f9db7 \
      --header "Authorization: Bearer $DEFAULT_API_KEY" \
      --header "Content-Type: application/json" \
      --data '{
        "email": "jordan@acme.com",
        "responses": {
          "company_size": "201-500",
          "use_case": "Route inbound demo requests"
        }
      }'
    ```

    The workflow runs your routing rules and responds with an execution id and an outcome.

    ```json theme={null}
    {
      "executionId": "9a8b7c6d-5e4f-4a3b-8c9d-0e1f2a3b4c5d",
      "outcome": {
        "type": "scheduler",
        "schedulerUrl": "https://book.default.com/acme/qualified-demo?workflowExecutionId=9a8b...",
        "schedulingLinkId": "a1a1a1a1-22b2-43c3-8d4d-5e5e5e5e5e5e"
      }
    }
    ```

    Keep both values. `outcome.type` tells you what the workflow decided:

    | Outcome     | Meaning                                                                                 |
    | ----------- | --------------------------------------------------------------------------------------- |
    | `scheduler` | The lead can book. Continue to the next step.                                           |
    | `redirect`  | The workflow chose to send the lead to `outcome.url` instead.                           |
    | `none`      | The workflow finished without offering a meeting. This is a valid result, not an error. |

    If you would rather show the calendar in a browser than book through the API, `schedulerUrl` is a
    ready to use booking page and you can stop here.

    Full reference: [`POST /v1/triggers/{trigger}`](/api-reference/triggers/fire-a-trigger)
  </Step>

  <Step title="Get available times">
    Ask for open slots, passing `outcome.schedulingLinkId` as the event. The link carries the routing
    decision, so the times you get back belong to the rep the workflow chose. Booking without a
    workflow works the same way, pass an event id or slug from your workspace instead and the times
    come from the event's own hosts.

    ```bash theme={null}
    curl --request POST https://api.default.com/v1/scheduling/events/a1a1a1a1-22b2-43c3-8d4d-5e5e5e5e5e5e/slots \
      --header "Authorization: Bearer $DEFAULT_API_KEY" \
      --header "Content-Type: application/json" \
      --data '{
        "start": "2026-08-20T00:00:00Z",
        "end": "2026-08-27T00:00:00Z"
      }'
    ```

    ```json theme={null}
    {
      "reservationId": "0f4c1c3a-95d5-4b0a-8a41-6a1b2c3d4e5f",
      "eventId": "3d9a7c21-6f0e-4d8b-a2c5-9e8f7a6b5c4d",
      "expiresAt": "2026-08-19T18:40:00.000Z",
      "slots": ["2026-08-20T16:00:00.000Z", "2026-08-20T16:30:00.000Z"],
      "activeHost": null
    }
    ```

    The range can span up to 63 days, nine weeks from whatever start you choose, roughly two months
    of availability per request. Keep the `reservationId` and pass it back if you query more date
    ranges, so the same host selection holds while the lead decides. Book before `expiresAt` or
    request slots again.

    Full reference: [`POST /v1/scheduling/events/{event}/slots`](/api-reference/scheduling/get-available-slots)
  </Step>

  <Step title="Book the meeting">
    Book a returned slot. Pass the same link id as `event`, the same email as `personEmail`, and the
    `executionId` from Step 2 as `workflowExecutionId`.

    ```bash theme={null}
    curl --request POST https://api.default.com/v1/scheduling/meetings \
      --header "Authorization: Bearer $DEFAULT_API_KEY" \
      --header "Content-Type: application/json" \
      --data '{
        "event": "a1a1a1a1-22b2-43c3-8d4d-5e5e5e5e5e5e",
        "reservationId": "0f4c1c3a-95d5-4b0a-8a41-6a1b2c3d4e5f",
        "startTime": "2026-08-20T16:00:00.000Z",
        "personEmail": "jordan@acme.com",
        "workflowExecutionId": "9a8b7c6d-5e4f-4a3b-8c9d-0e1f2a3b4c5d",
        "guestFirstName": "Jordan"
      }'
    ```

    ```json theme={null}
    {
      "id": "7b0d3c58-3b8f-4b52-9b6a-2f4f2f9a1a10",
      "status": "scheduled",
      "date": "2026-08-20T16:00:00.000Z",
      "duration": 30,
      "meetingLink": "https://meet.google.com/abc-defg-hij",
      "personEmail": "jordan@acme.com"
    }
    ```

    The `workflowExecutionId` is what ties the meeting back to the routing decision. It resumes the
    workflow, so CRM writeback, notifications, and routing fairness all record against this booking.
    Without it the meeting still books, but the workflow never learns about it.

    Full reference: [`POST /v1/scheduling/meetings`](/api-reference/scheduling/book-a-meeting)
  </Step>
</Steps>

## Handling errors

Every error comes back as `{ "error": { "code", "message" } }`.

| Status | Code                          | What happened                                                                         | What to do                                                                                                        |
| ------ | ----------------------------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| 400    | `INVALID_REQUEST` on fire     | A required field is missing or an email value conflicts                               | The message names the response keys to fix                                                                        |
| 400    | `WORK_EMAIL_REQUIRED`         | The form requires a work email and got a personal one                                 | Ask the lead for their work email                                                                                 |
| 404    | `NOT_FOUND`                   | The trigger, event, or meeting id does not exist in your workspace                    | Check the id, list triggers or events again                                                                       |
| 409    | `SLOT_TAKEN` on book          | Someone took the time first                                                           | Fetch slots again with the same `reservationId` and retry                                                         |
| 409    | `RESERVATION_EXPIRED` on book | The slot hold lapsed before booking                                                   | Fetch slots again without a `reservationId` and book the new one                                                  |
| 409    | `RESERVATION_USED` on book    | The reservation was already spent, usually by an earlier book whose response was lost | Treat the meeting as booked, see the note below                                                                   |
| 409    | `CONFLICT` on book            | The scheduling session ended                                                          | If your book request just timed out, see the note below. Otherwise fire the trigger again and restart from step 2 |
| 429    | `RATE_LIMITED`                | More than 30 requests in a minute on this key                                         | Wait for the seconds in the `Retry-After` header                                                                  |

Each endpoint's reference page lists the exact codes it can return per status, with example payloads.

**When a book response is lost.** If your book request times out, do not book again. The booking
almost always succeeded, and the meeting invite reaches the lead's calendar either way. A retry
with the same `workflowExecutionId` returns `CONFLICT` or `RESERVATION_USED` and can never create
a second meeting for the same workflow run. A retry without a workflow can even return
`RESERVATION_EXPIRED` after the hold lapses, so treat any of these three codes after a timeout as
a sign the meeting exists, not an invitation to rebook.

Every fire starts a new workflow run, so nodes before the scheduler run again. If your workflow
writes to a CRM or sends notifications, expect a repeat fire to do that work again, and a step
that assigns the routed rep, like writing an owner to your CRM, counts against routing fairness
on each run. The booking itself is safe to retry, a meeting only counts once it actually books.
When you already have a scheduler outcome, reuse it instead of firing again.
