> ## 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.

# Onboarding API

> Onboarding endpoints exposed by the Devotel CPaaS API

# Onboarding API

Onboarding endpoints exposed by the Devotel CPaaS API

**Base path:** `/api/v1/onboarding`

**Endpoint count:** 6

***

### Get workspace provisioning progress

<Note>
  `GET /api/v1/onboarding/provisioning-status`
</Note>

Real-time provisioning signals polled by the post-signup provisioning panel: whether the operator's email is verified, the tenant schema has been created, trial credits have been granted (informational only), and at least one API key has been seeded. `readyCount` and `allReady` are computed from the three blocking signals (email, schema, API key). Poll at \~2s intervals during signup; responses are cached 5s so the panel advances rows with sub-second lag.

<ParamField header="X-Test-Mode" type="string (enum: true|false)">
  Sandbox opt-in for Clerk-session-authenticated requests. Set to `true` to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response `meta.test_mode: true`. **Ignored for live API keys (`dv_live_sk_*`)** — server-to-server clients must use a test-prefixed key (`dv_test_sk_*`) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.
</ParamField>

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X GET "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}
    import { Orbit } from '@devotel-orbit/node'

    const orbit = new Orbit({
      apiKey: process.env.ORBIT_API_KEY!,
    })

    const res = await fetch('https://api.orbit.devotel.io/api/v1/onboarding/provisioning-status', {
      method: 'GET',
      headers: {
        'X-API-Key': process.env.ORBIT_API_KEY!,
      },
    })
    console.log(await res.json())


    ```

    ```python Python theme={null}
    import os, requests

    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.get("https://api.orbit.devotel.io/api/v1/onboarding/provisioning-status", headers=headers)
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"bytes"
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("GET", "https://api.orbit.devotel.io/api/v1/onboarding/provisioning-status", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))

    	http.DefaultClient.Do(req)
    }
    ```

    ```ruby Ruby theme={null}
    require 'net/http'
    require 'json'

    uri = URI('https://api.orbit.devotel.io/api/v1/onboarding/provisioning-status')
    req = Net::HTTP::Get.new(uri)
    req['X-API-Key'] = ENV['ORBIT_API_KEY']


    res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
    puts res.body
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/onboarding/provisioning-status');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'X-API-Key: ' . getenv('ORBIT_API_KEY'),

    ]);

    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

<ResponseExample>
  ```json 200 theme={null}
  {}
  ```
</ResponseExample>

***

### Get the SMS getting-started checklist

<Note>
  `GET /api/v1/onboarding/sms-checklist`
</Note>

Channel-scoped fast-path for the SMS surface's 5-item getting-started panel: whether the org has an active API key, a purchased number, an approved SMS sender ID, a sent SMS message, and an active webhook endpoint. Collapses six dashboard queries into one round-trip; results are cached 30s. Use it to render the SMS onboarding progress header (`completedCount` of 5) without hitting six endpoints.

<ParamField header="X-Test-Mode" type="string (enum: true|false)">
  Sandbox opt-in for Clerk-session-authenticated requests. Set to `true` to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response `meta.test_mode: true`. **Ignored for live API keys (`dv_live_sk_*`)** — server-to-server clients must use a test-prefixed key (`dv_test_sk_*`) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.
</ParamField>

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X GET "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}
    import { Orbit } from '@devotel-orbit/node'

    const orbit = new Orbit({
      apiKey: process.env.ORBIT_API_KEY!,
    })

    const res = await fetch('https://api.orbit.devotel.io/api/v1/onboarding/sms-checklist', {
      method: 'GET',
      headers: {
        'X-API-Key': process.env.ORBIT_API_KEY!,
      },
    })
    console.log(await res.json())


    ```

    ```python Python theme={null}
    import os, requests

    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.get("https://api.orbit.devotel.io/api/v1/onboarding/sms-checklist", headers=headers)
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"bytes"
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("GET", "https://api.orbit.devotel.io/api/v1/onboarding/sms-checklist", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))

    	http.DefaultClient.Do(req)
    }
    ```

    ```ruby Ruby theme={null}
    require 'net/http'
    require 'json'

    uri = URI('https://api.orbit.devotel.io/api/v1/onboarding/sms-checklist')
    req = Net::HTTP::Get.new(uri)
    req['X-API-Key'] = ENV['ORBIT_API_KEY']


    res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
    puts res.body
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/onboarding/sms-checklist');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'X-API-Key: ' . getenv('ORBIT_API_KEY'),

    ]);

    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

<ResponseExample>
  ```json 200 theme={null}
  {}
  ```
</ResponseExample>

***

### Get the getting-started checklist

<Note>
  `GET /api/v1/onboarding/status`
</Note>

Server-derived 9-step getting-started checklist (verify email, create + name the workspace, add a payment method, receive the trial credit, buy a number, send a message, add a contact, and build a campaign/agent/invite). Each step reports `completed`, `completedAt`, and whether completion came from the platform's own probes (`system`) or a manual override (`user`), plus the next incomplete step for the resume banner. Poll this to render or hide the dashboard checklist — responses are cached 60s per (org, user) and degrade to an all-incomplete 200 when the backend is briefly unavailable, so the UI never throws on a transient miss.

<ParamField header="X-Test-Mode" type="string (enum: true|false)">
  Sandbox opt-in for Clerk-session-authenticated requests. Set to `true` to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response `meta.test_mode: true`. **Ignored for live API keys (`dv_live_sk_*`)** — server-to-server clients must use a test-prefixed key (`dv_test_sk_*`) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.
</ParamField>

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

    ```typescript Node.js theme={null}
    import { Orbit } from '@devotel-orbit/node'

    const orbit = new Orbit({
      apiKey: process.env.ORBIT_API_KEY!,
    })

    const res = await fetch('https://api.orbit.devotel.io/api/v1/onboarding/status', {
      method: 'GET',
      headers: {
        'X-API-Key': process.env.ORBIT_API_KEY!,
      },
    })
    console.log(await res.json())


    ```

    ```python Python theme={null}
    import os, requests

    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.get("https://api.orbit.devotel.io/api/v1/onboarding/status", headers=headers)
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"bytes"
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("GET", "https://api.orbit.devotel.io/api/v1/onboarding/status", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))

    	http.DefaultClient.Do(req)
    }
    ```

    ```ruby Ruby theme={null}
    require 'net/http'
    require 'json'

    uri = URI('https://api.orbit.devotel.io/api/v1/onboarding/status')
    req = Net::HTTP::Get.new(uri)
    req['X-API-Key'] = ENV['ORBIT_API_KEY']


    res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
    puts res.body
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/onboarding/status');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'X-API-Key: ' . getenv('ORBIT_API_KEY'),

    ]);

    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

<ResponseExample>
  ```json 200 theme={null}
  {}
  ```
</ResponseExample>

***

### Get the compliance status timeline

<Note>
  `GET /api/v1/onboarding/status-timeline`
</Note>

Aggregated self-service verification timeline across account KYC, US 10DLC brands + campaigns, per-country compliance profiles, and per-number compliance state. Each row reports a folded state (`pending` / `in_review` / `approved` / `rejected` / `not_applicable`), submitted and updated timestamps, a documented SLA + computed ETA when the underlying service defines one (KYC only: 24h), the current blocker line, and a deep-link `resolveUrl` to fix it. Use it to power one where-verification-is-stuck view instead of polling five services; cached 60s per org.

<ParamField header="X-Test-Mode" type="string (enum: true|false)">
  Sandbox opt-in for Clerk-session-authenticated requests. Set to `true` to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response `meta.test_mode: true`. **Ignored for live API keys (`dv_live_sk_*`)** — server-to-server clients must use a test-prefixed key (`dv_test_sk_*`) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.
</ParamField>

<RequestExample>
  <CodeGroup>
    ```bash cURL theme={null}
    curl -X GET "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}
    import { Orbit } from '@devotel-orbit/node'

    const orbit = new Orbit({
      apiKey: process.env.ORBIT_API_KEY!,
    })

    const res = await fetch('https://api.orbit.devotel.io/api/v1/onboarding/status-timeline', {
      method: 'GET',
      headers: {
        'X-API-Key': process.env.ORBIT_API_KEY!,
      },
    })
    console.log(await res.json())


    ```

    ```python Python theme={null}
    import os, requests

    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    r = requests.get("https://api.orbit.devotel.io/api/v1/onboarding/status-timeline", headers=headers)
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"bytes"
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("GET", "https://api.orbit.devotel.io/api/v1/onboarding/status-timeline", nil)
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))

    	http.DefaultClient.Do(req)
    }
    ```

    ```ruby Ruby theme={null}
    require 'net/http'
    require 'json'

    uri = URI('https://api.orbit.devotel.io/api/v1/onboarding/status-timeline')
    req = Net::HTTP::Get.new(uri)
    req['X-API-Key'] = ENV['ORBIT_API_KEY']


    res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
    puts res.body
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/onboarding/status-timeline');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'X-API-Key: ' . getenv('ORBIT_API_KEY'),

    ]);

    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

<ResponseExample>
  ```json 200 theme={null}
  {}
  ```
</ResponseExample>

***

### Mark an onboarding step complete

<Note>
  `POST /api/v1/onboarding/complete-step`
</Note>

Manually mark one of the nine canonical onboarding steps as done for the calling user: stores a per-(org, user, step) override that the status derivation merges on top of its automatic probes, so a step that isn't auto-detectable (e.g. a step completed in the operator's own tooling) still clears from the checklist. Powers the dashboard's Mark-as-done affordance; admin-or-higher only and audit-logged.

<ParamField header="Idempotency-Key" type="string">
  Stripe-style idempotency token. Pass a stable, client-generated value (1-255 chars) to dedupe retries on transient timeouts. The same key+credential+path replays the original response for 24h on 2xx (5min on 4xx, 30s on 5xx). Returns 409 if a concurrent request with the same key is already in flight; replayed responses include the `Idempotency-Replay: true` response header.
</ParamField>

<ParamField header="X-Test-Mode" type="string (enum: true|false)">
  Sandbox opt-in for Clerk-session-authenticated requests. Set to `true` to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response `meta.test_mode: true`. **Ignored for live API keys (`dv_live_sk_*`)** — server-to-server clients must use a test-prefixed key (`dv_test_sk_*`) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.
</ParamField>

<ParamField body="stepId" type="string (enum: email_verified|org_created|org_named|payment_method_added|trial_credit_granted|first_number_purchased|…)" required>
  One of the nine canonical onboarding step ids (e.g. `first_number_purchased`).
</ParamField>

<RequestExample>
  <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 "Content-Type: application/json" \
      -d '{
      "stepId": "email_verified"
    }'
    ```

    ```typescript Node.js theme={null}
    import { Orbit } from '@devotel-orbit/node'

    const orbit = new Orbit({
      apiKey: process.env.ORBIT_API_KEY!,
    })

    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!,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
      "stepId": "email_verified"
    }),
    })
    console.log(await res.json())
    ```

    ```python Python theme={null}
    import os, requests

    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    headers["Content-Type"] = "application/json"
    r = requests.post("https://api.orbit.devotel.io/api/v1/onboarding/complete-step", headers=headers, json={
      "stepId": "email_verified"
    })
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"bytes"
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST", "https://api.orbit.devotel.io/api/v1/onboarding/complete-step", bytes.NewBuffer([]byte(`{
      "stepId": "email_verified"
    }`)))
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	req.Header.Set("Content-Type", "application/json")
    	http.DefaultClient.Do(req)
    }
    ```

    ```ruby Ruby theme={null}
    require 'net/http'
    require 'json'

    uri = URI('https://api.orbit.devotel.io/api/v1/onboarding/complete-step')
    req = Net::HTTP::Post.new(uri)
    req['X-API-Key'] = ENV['ORBIT_API_KEY']
    req['Content-Type'] = 'application/json'
    req.body = {
      "stepId": "email_verified"
    }.to_json
    res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
    puts res.body
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/onboarding/complete-step');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'X-API-Key: ' . getenv('ORBIT_API_KEY'),
      'Content-Type: application/json',
    ]);
    curl_setopt($ch, CURLOPT_POSTFIELDS, <<<JSON
    {
      "stepId": "email_verified"
    }
    JSON);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

<ResponseExample>
  ```json 200 theme={null}
  {}
  ```

  ```json 422 Error theme={null}
  {}
  ```
</ResponseExample>

***

### Skip an onboarding step

<Note>
  `POST /api/v1/onboarding/skip-step`
</Note>

Dismiss one of the nine canonical onboarding steps that does not apply to the org (e.g. `payment_method_added` on a sandbox-only workspace). Behaves exactly like completing the step — the override row is marked `skipped`, the step surfaces as completed with `completedBy: 'user'` in the status response, and the distinction is retained for product analytics. Admin-or-higher only; the change is audit-logged.

<ParamField header="Idempotency-Key" type="string">
  Stripe-style idempotency token. Pass a stable, client-generated value (1-255 chars) to dedupe retries on transient timeouts. The same key+credential+path replays the original response for 24h on 2xx (5min on 4xx, 30s on 5xx). Returns 409 if a concurrent request with the same key is already in flight; replayed responses include the `Idempotency-Replay: true` response header.
</ParamField>

<ParamField header="X-Test-Mode" type="string (enum: true|false)">
  Sandbox opt-in for Clerk-session-authenticated requests. Set to `true` to route the call through the test-mode pipeline: no real provider delivery, no credits deducted, response `meta.test_mode: true`. **Ignored for live API keys (`dv_live_sk_*`)** — server-to-server clients must use a test-prefixed key (`dv_test_sk_*`) to exercise sandbox. Test-prefixed keys unconditionally enable sandbox regardless of this header.
</ParamField>

<ParamField body="stepId" type="string (enum: email_verified|org_created|org_named|payment_method_added|trial_credit_granted|first_number_purchased|…)" required>
  One of the nine canonical onboarding step ids (e.g. `first_number_purchased`).
</ParamField>

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

    ```typescript Node.js theme={null}
    import { Orbit } from '@devotel-orbit/node'

    const orbit = new Orbit({
      apiKey: process.env.ORBIT_API_KEY!,
    })

    const res = await fetch('https://api.orbit.devotel.io/api/v1/onboarding/skip-step', {
      method: 'POST',
      headers: {
        'X-API-Key': process.env.ORBIT_API_KEY!,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
      "stepId": "email_verified"
    }),
    })
    console.log(await res.json())
    ```

    ```python Python theme={null}
    import os, requests

    headers = {"X-API-Key": os.environ["ORBIT_API_KEY"]}
    headers["Content-Type"] = "application/json"
    r = requests.post("https://api.orbit.devotel.io/api/v1/onboarding/skip-step", headers=headers, json={
      "stepId": "email_verified"
    })
    print(r.json())
    ```

    ```go Go theme={null}
    package main

    import (
    	"bytes"
    	"net/http"
    	"os"
    )

    func main() {
    	req, _ := http.NewRequest("POST", "https://api.orbit.devotel.io/api/v1/onboarding/skip-step", bytes.NewBuffer([]byte(`{
      "stepId": "email_verified"
    }`)))
    	req.Header.Set("X-API-Key", os.Getenv("ORBIT_API_KEY"))
    	req.Header.Set("Content-Type", "application/json")
    	http.DefaultClient.Do(req)
    }
    ```

    ```ruby Ruby theme={null}
    require 'net/http'
    require 'json'

    uri = URI('https://api.orbit.devotel.io/api/v1/onboarding/skip-step')
    req = Net::HTTP::Post.new(uri)
    req['X-API-Key'] = ENV['ORBIT_API_KEY']
    req['Content-Type'] = 'application/json'
    req.body = {
      "stepId": "email_verified"
    }.to_json
    res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
    puts res.body
    ```

    ```php PHP theme={null}
    <?php
    $ch = curl_init('https://api.orbit.devotel.io/api/v1/onboarding/skip-step');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'X-API-Key: ' . getenv('ORBIT_API_KEY'),
      'Content-Type: application/json',
    ]);
    curl_setopt($ch, CURLOPT_POSTFIELDS, <<<JSON
    {
      "stepId": "email_verified"
    }
    JSON);
    echo curl_exec($ch);
    ```
  </CodeGroup>
</RequestExample>

<ResponseExample>
  ```json 200 theme={null}
  {}
  ```

  ```json 422 Error theme={null}
  {}
  ```
</ResponseExample>

***
