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

# Worked onboarding reads

> Provisioning poll, the checklist-driven bootstrap chain, manual step overrides, and the compliance timeline with canonical envelopes.

## Worked onboarding reads

The onboarding endpoints are read-mostly state probes — the account estate a
new tenant works through in its first session is easiest to learn as one
chain: **poll the provisioning panel → drive the checklist → override the
steps that don't auto-detect → confirm channel readiness → watch where
verification is stuck.** The samples below walk that chain end to end and
show the full `{ data, meta }` envelope at each step.

Scope of this overlay: the samples show cURL and Node — the two
most-requested languages. Python, Go, Ruby, and PHP tabs appear on the
operation blocks themselves. The quickstart ([quickstart](/quickstart))
chains the same steps with dashboard screenshots; this overlay stays
envelope-accurate against the API.

### 1. Poll the provisioning panel

Right after signup, poll `GET /api/v1/onboarding/provisioning-status` at \~2s
intervals until `allReady` is `true`. `emailVerified`, `tenantSchemaReady`,
and `apiKeySeeded` are the three blocking signals — the first API key
(`dv_live_sk_*` / `dv_test_sk_*`) is seeded by the workspace itself during
provisioning, so this read is also how you confirm you can start signing API
calls. `trialCreditsGranted` is reported for information but never blocks.

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://api.orbit.devotel.io/api/v1/onboarding/provisioning-status" \
    -H "X-API-Key: dv_live_sk_your_key_here"
  ```

  ```typescript Node.js theme={null}
  const res = await fetch(
    "https://api.orbit.devotel.io/api/v1/onboarding/provisioning-status",
    { headers: { "X-API-Key": process.env.ORBIT_API_KEY! } },
  );
  const { data } = await res.json();
  // Advance one panel row per boolean; stop polling when data.allReady flips.
  console.log(data.readyCount, data.allReady);
  ```
</CodeGroup>

```json 200 theme={null}
{
  "data": {
    "emailVerified": true,
    "tenantSchemaReady": true,
    "trialCreditsGranted": false,
    "apiKeySeeded": true,
    "readyCount": 3,
    "allReady": true
  },
  "meta": {
    "request_id": "req_7c2e91ab",
    "timestamp": "2026-09-09T10:00:04.000Z"
  }
}
```

Once `allReady` is `true`, stop polling this endpoint — the 5s cache exists
for the provisioning window only.

### 2. Drive the bootstrap chain off the checklist

`GET /api/v1/onboarding/status` returns the nine-step getting-started
checklist with every satisfaction probe already resolved server-side. Each
step carries an `id`, a dashboard deep link (`ctaUrl`), a `completed` flag,
and `completedBy` — `system` when the platform's own probe saw the signal,
`user` when an operator marked or skipped it. Branch your integration on
`nextIncompleteStep` instead of diffing the steps client-side.

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://api.orbit.devotel.io/api/v1/onboarding/status" \
    -H "X-API-Key: dv_live_sk_your_key_here"
  ```

  ```typescript Node.js theme={null}
  const res = await fetch(
    "https://api.orbit.devotel.io/api/v1/onboarding/status",
    { headers: { "X-API-Key": process.env.ORBIT_API_KEY! } },
  );
  const { data } = await res.json();
  // Resume banner reads the first incomplete step directly.
  console.log(data.nextIncompleteStep?.id, data.overallPct);
  ```
</CodeGroup>

```json 200 theme={null}
{
  "data": {
    "steps": [
      {
        "id": "email_verified",
        "label": "Verify your email",
        "description": "Confirm your email address to unlock account recovery and security alerts.",
        "ctaUrl": "/settings/profile",
        "completed": true,
        "completedAt": "2026-09-09T09:41:12.000Z",
        "completedBy": "system"
      },
      {
        "id": "org_created",
        "label": "Create your workspace",
        "description": "Set up a workspace so you can invite teammates and keep data scoped per tenant.",
        "ctaUrl": "/settings/general",
        "completed": true,
        "completedAt": "2026-09-09T09:47:30.000Z",
        "completedBy": "system"
      },
      {
        "id": "org_named",
        "label": "Name your workspace",
        "description": "Rename your workspace from 'Personal' so teammates recognise it in their org switcher.",
        "ctaUrl": "/settings/general",
        "completed": true,
        "completedAt": "2026-09-09T09:48:05.000Z",
        "completedBy": "system"
      },
      {
        "id": "payment_method_added",
        "label": "Add a payment method",
        "description": "Attach a card or bank account so live-mode messages and number purchases can settle.",
        "ctaUrl": "/billing/payment",
        "completed": false,
        "completedAt": null,
        "completedBy": null
      },
      {
        "id": "trial_credit_granted",
        "label": "Receive your trial credit",
        "description": "KYC submitted — under review (usually < 24h).",
        "ctaUrl": "/settings/kyc",
        "completed": false,
        "completedAt": null,
        "completedBy": null
      }
    ],
    "totalCount": 9,
    "completedCount": 3,
    "complete": false,
    "completedAt": null,
    "overallPct": 33,
    "nextIncompleteStep": {
      "id": "payment_method_added",
      "label": "Add a payment method",
      "ctaUrl": "/billing/payment"
    }
  },
  "meta": {
    "request_id": "req_7c2e920c",
    "timestamp": "2026-09-09T10:01:02.000Z"
  }
}
```

Three contract notes callers branch on:

* **`trial_credit_granted` is the wallet confirmation.** It flips only after
  account verification (KYC) is approved and the trial credit lands; until
  verification is approved the step is hidden from `steps[]` entirely, and
  `totalCount` drops to 8 — compute progress from `completedCount` /
  `totalCount`, never from a hard-coded nine.
* **`payment_method_added` is the plan-credit attachment step** — it flips
  the moment a default card or bank account exists for billing, and live
  traffic and number purchases settle against it.
* **A backend blip degrades, never errors.** When the platform probes are
  briefly unavailable the endpoint still returns `200` with every step
  reading incomplete (the response is not cached, so the next poll recovers
  the true state). Treat an all-incomplete payload as "ask again in a minute",
  not as a failed call — and keep your poll cadence at or above the 60s cache.

### 3. Mark or skip the steps that don't auto-detect

Steps that resolve outside the platform's reach need a manual nudge.
`POST /api/v1/onboarding/complete-step` and `/skip-step` store a per-user
override that the checklist derivation merges on top of its probes — the
step then reports `completed: true` with `completedBy: "user"`. Both writes
are restricted to the owner and admin roles and are audit-logged; send an
`Idempotency-Key` so a retried click never double-writes.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.orbit.devotel.io/api/v1/onboarding/complete-step" \
    -H "X-API-Key: dv_live_sk_your_key_here" \
    -H "Idempotency-Key: onboarding-complete-payment-method" \
    -H "Content-Type: application/json" \
    -d '{ "stepId": "payment_method_added" }'
  ```

  ```typescript Node.js theme={null}
  const res = await fetch(
    "https://api.orbit.devotel.io/api/v1/onboarding/complete-step",
    {
      method: "POST",
      headers: {
        "X-API-Key": process.env.ORBIT_API_KEY!,
        "Idempotency-Key": "onboarding-complete-payment-method",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ stepId: "payment_method_added" }),
    },
  );
  console.log(await res.json());
  ```
</CodeGroup>

```json 200 theme={null}
{
  "data": {
    "stepId": "payment_method_added",
    "action": "manual_complete"
  },
  "meta": {
    "request_id": "req_7c2e93d1",
    "timestamp": "2026-09-09T10:02:11.000Z"
  }
}
```

Use the same body for `/skip-step`; its echo carries `"action": "skipped"`.
Skip is the right call for steps that genuinely don't apply — for example a
sandbox-only workspace that never adds a payment method. A step id outside
the nine canonical values is rejected:

```json 422 theme={null}
{
  "error": {
    "code": "INVALID_REQUEST",
    "status": 422,
    "message": "Invalid onboarding step id. Allowed: email_verified, org_created, org_named, payment_method_added, trial_credit_granted, first_number_purchased, first_message_sent, first_contact_created, first_agent_or_campaign_or_team_invite"
  },
  "meta": {
    "request_id": "req_7c2e94e2",
    "timestamp": "2026-09-09T10:02:14.000Z"
  }
}
```

### 4. Confirm SMS readiness in one read

For the SMS surface specifically, `GET /api/v1/onboarding/sms-checklist`
collapses five readiness probes — API key, purchased number, approved SMS
sender ID, first sent message, active webhook — into one round-trip. It is
the fast path to gate a "you can send real traffic now" state without
hitting five endpoints.

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://api.orbit.devotel.io/api/v1/onboarding/sms-checklist" \
    -H "X-API-Key: dv_live_sk_your_key_here"
  ```

  ```typescript Node.js theme={null}
  const res = await fetch(
    "https://api.orbit.devotel.io/api/v1/onboarding/sms-checklist",
    { headers: { "X-API-Key": process.env.ORBIT_API_KEY! } },
  );
  const { data } = await res.json();
  // "4/5 complete" header without re-counting client-side.
  console.log(`${data.completedCount}/5 complete`);
  ```
</CodeGroup>

```json 200 theme={null}
{
  "data": {
    "hasApiKey": true,
    "hasNumber": true,
    "hasSenderId": true,
    "hasSentMessage": true,
    "hasWebhook": false,
    "completedCount": 4
  },
  "meta": {
    "request_id": "req_7c2e95f3",
    "timestamp": "2026-09-09T10:03:22.000Z"
  }
}
```

Note `hasSenderId` counts only an **approved** SMS sender — a registration
still pending provider approval reads `false`.

### 5. See where verification is stuck

Once traffic is moving, the remaining onboarding risk is verification lag.
`GET /api/v1/onboarding/status-timeline` folds account KYC, US 10DLC brand
and campaign registration, per-country compliance profiles, and per-number
compliance state into one ordered timeline — each row carries its folded
`state`, the current `blocker` line, and a deep-link `resolveUrl` to fix it.

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://api.orbit.devotel.io/api/v1/onboarding/status-timeline" \
    -H "X-API-Key: dv_live_sk_your_key_here"
  ```

  ```typescript Node.js theme={null}
  const res = await fetch(
    "https://api.orbit.devotel.io/api/v1/onboarding/status-timeline",
    { headers: { "X-API-Key": process.env.ORBIT_API_KEY! } },
  );
  const { data } = await res.json();
  // Render one "where verification is stuck" view from the folded states.
  for (const step of data.steps) {
    if (step.state === "rejected" || step.blocker) {
      console.log(step.id, step.state, step.blocker, step.resolveUrl);
    }
  }
  ```
</CodeGroup>

```json 200 theme={null}
{
  "data": {
    "steps": [
      {
        "id": "kyc:account",
        "kind": "kyc",
        "label": "Account verification",
        "state": "in_review",
        "submittedAt": "2026-09-08T14:12:00.000Z",
        "updatedAt": "2026-09-09T09:41:00.000Z",
        "sla": { "hours": 24, "label": "24h" },
        "etaAt": "2026-09-09T14:12:00.000Z",
        "blocker": null,
        "resolveUrl": "/settings/kyc",
        "reviewer": null
      },
      {
        "id": "10dlc:brand",
        "kind": "us_10dlc",
        "label": "US 10DLC brand",
        "state": "pending",
        "submittedAt": "2026-09-08T15:01:00.000Z",
        "updatedAt": "2026-09-08T15:01:00.000Z",
        "sla": null,
        "etaAt": null,
        "blocker": "Brand registration is waiting on the campaign submission.",
        "resolveUrl": "/compliance/senders",
        "reviewer": null
      }
    ],
    "counts": {
      "total": 2,
      "approved": 0,
      "in_review": 1,
      "rejected": 0,
      "pending": 1
    },
    "computedAt": "2026-09-09T10:04:01.000Z"
  },
  "meta": {
    "request_id": "req_7c2e9604",
    "timestamp": "2026-09-09T10:04:01.000Z"
  }
}
```

Poll this at or above its 60s cache cadence — a row that sits in
`in_review` past its `etaAt` is the one to surface, not to re-fetch faster.
