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

# Migrating from the Calls API

> Move from the legacy Calls API to the Requests API before the October 1, 2026 sunset.

The legacy **Calls API** (`POST /v1/calls`, `scriptId`) is replaced by the **Requests API**
(`POST /v1/requests`, `schemaId`). Legacy endpoints sunset on **October 1, 2026**.

The dialing capability is the same. What changes is the shape of the integration: a
[request](/guides/concepts#request) replaces a call as the unit of work, a
[schema](/guides/concepts#schema) replaces a script, and results come back as schema-defined named
fields instead of question-indexed answers.

Several things you might assume are new already exist in the legacy API: the `GET /v1/auth` token
exchange, structured errors, and definition listing. This guide calls those out so you don't rebuild
what you already have.

<Warning>
  Two changes break silently if you miss them. The dialed number moves from a top-level `to` field
  into `inputs.phoneNumber` (see [Submitting work](#3-submitting-work)), and the Requests API needs
  its **own** webhook URL, separate from your legacy one (see
  [Configure the Request webhook URL](#configure-the-request-webhook-url)). Neither returns an
  error when you get it wrong.
</Warning>

## 1. At a glance

| Concept           | Legacy (Calls API)                                        | New (Requests API)                                                              |
| ----------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Unit of work      | Call                                                      | Request (may be fulfilled digitally, by phone, or both)                         |
| Definition object | Script (`scriptId`)                                       | Schema (`schemaId`)                                                             |
| Submit            | `POST /v1/calls`                                          | `POST /v1/requests`                                                             |
| Fetch one         | `GET /v1/calls/{callId}`                                  | `GET /v1/requests/{requestId}`                                                  |
| List              | `GET /v1/calls`                                           | `GET /v1/requests`                                                              |
| Discover inputs   | `GET /v1/scripts/{scriptId}/required-inputs`              | `GET /v1/schemas/{schemaId}/required-inputs`                                    |
| List definitions  | `GET /v1/scripts`                                         | `GET /v1/schemas`                                                               |
| Authentication    | `GET /v1/auth`                                            | `GET /v1/auth` (same endpoint)                                                  |
| Dialed number     | top-level `to`                                            | `inputs.phoneNumber`                                                            |
| States            | `processing`, `success`, `failure`, `cancelled`, `paused` | `PROCESSING`, `SUCCESS`, `PARTIAL`, `FAILURE`                                   |
| Results shape     | question-indexed answers (`"0"`, `"0.0"`)                 | schema-defined named fields (`results.claimStatus`)                             |
| Errors            | `error` string plus an `errors` object                    | structured [`error`](/guides/concepts#error-object) object plus `missingFields` |
| Webhook URL       | one setting                                               | a separate setting, configured independently                                    |

The base host does not change: `https://robodialer-service-api-9nc4t1p9.uc.gateway.dev`. Sandbox and
production continue to use separate credentials against that one host.

## 2. Authentication

Authentication is **unchanged**. Both APIs use the same `GET /v1/auth` exchange: trade your
long-lived key and secret for a short-lived bearer token, then attach that token to every subsequent
request.

```bash theme={null}
curl "https://robodialer-service-api-9nc4t1p9.uc.gateway.dev/v1/auth" \
  -H "Robodialer-API-Key: <your-api-key>" \
  -H "Robodialer-API-Secret: <your-api-secret>"
```

```json theme={null}
{ "token": "<bearer-token>" }
```

Pass it as `Authorization: Bearer <token>`. Tokens are valid for one hour.

<Note>
  No change required if you already exchange credentials at `GET /v1/auth` and cache the token
  (up to about 55 minutes, refreshing before expiry). Keep your key and secret in a secret store: they are long-lived,
  the token is not. See [Production vs Sandbox](/guides/concepts#production-vs-sandbox) for which
  key to use when.
</Note>

## 3. Submitting work

Rename `scriptId` to `schemaId`, post to `/v1/requests`, and **move the dialed number into
`inputs.phoneNumber`**. `internalId`, `internalTag`, and `webhookUrl` carry over unchanged.

### Legacy: POST /v1/calls

```json theme={null}
{
  "scriptId": "123456",
  "to": "2125551234",
  "inputs": {
    "memberId": "TEST123456789",
    "payerName": "Sample Insurance Co",
    "memberDateOfBirth": "1990-01-01"
  }
}
```

```json theme={null}
{ "callId": "call_abc123xyz", "batchId": "OEcqR7tYuI2pLm4nBv8k" }
```

### New: POST /v1/requests

```json theme={null}
{
  "schemaId": "fWxzG4nqtpHsJxS5Lm3q",
  "inputs": {
    "phoneNumber": "2125551234",
    "memberId": "TEST123456789",
    "payerName": "Sample Insurance Co",
    "memberDateOfBirth": "1990-01-01"
  },
  "internalId": "crm-ticket-441255",
  "internalTag": "vob-prod",
  "webhookUrl": "https://your-app.example.com/webhooks/superdial/requests"
}
```

```json theme={null}
{
  "requestId": "8bF7xK2mP9qR4sT6uV0w",
  "requestBatchId": "pH9kJ2lM4nB6vC8xZ7Qr",
  "internalId": "crm-ticket-441255",
  "payerLookup": { }
}
```

<Warning>
  **There is no top-level `to` field on `POST /v1/requests`.** The dialed number is an input:
  `inputs.phoneNumber`. A body that sends `to` at the top level is not rejected, because unknown
  top-level keys are ignored. Instead the request fails input validation for a missing
  `phoneNumber`, or, if your account has payer phone number lookup enabled, it silently dials a
  number you did not choose. `to` does still appear in the **response**, where it reports the number
  actually dialed.
</Warning>

Field mapping: `scriptId` to `schemaId`, `to` to `inputs.phoneNumber`, `callId` to `requestId`,
`batchId` to `requestBatchId`. `inputs`, `internalId`, and `internalTag` carry through unchanged.
`webhookUrl` remains an optional per-request override.

The create response also always carries a `payerLookup` object describing how the payer and number
were resolved. See [Creating a Request](/guides/creating-a-request#single-request) for the full
field list.

### Batch submission

Rename the wrapper key `calls` to `requests`.

```json theme={null}
{ "requests": [
  { "schemaId": "fWxzG4nqtpHsJxS5Lm3q", "inputs": { "phoneNumber": "2125551234", "memberId": "TEST111" } },
  { "schemaId": "fWxzG4nqtpHsJxS5Lm3q", "inputs": { "phoneNumber": "2125551234", "memberId": "TEST222" } }
]}
```

The batch response returns a per-item array, and an item may carry an `error` instead of a
`requestId`, so check every element. Note two differences from legacy: the per-item error key is
singular `error` (legacy used plural `errors`), and the aggregate status code is not simply 207. A
batch returns 200 when every item succeeds, **500 if any item failed with a server error**, 207 for
a mix of successes and client errors, and 400 when every item failed with a client error. Treat 500
as "partial results may still be present" rather than "nothing was created". See
[Batch](/guides/creating-a-request#batch).

## 4. Discovering inputs and schemas

You can enumerate your provisioned schemas programmatically. This is not new capability: the legacy
API already exposes `GET /v1/scripts`, which returns your scripts as
`[{ "id", "type", "name", "createdAt" }]`. `GET /v1/schemas` is its direct replacement. Map `id` to
`schemaId` and `type` to `requestType`.

```bash theme={null}
curl "https://robodialer-service-api-9nc4t1p9.uc.gateway.dev/v1/schemas" \
  -H "Authorization: Bearer <token>"
```

```json theme={null}
{ "schemas": [
  { "schemaId": "fWxzG4nqtpHsJxS5Lm3q", "name": "Claim Status (Commercial)", "requestType": "claim-status" },
  { "schemaId": "qP2bN8rT6mK1xC3vW9aL", "name": "Verification of Benefits", "requestType": "vob" }
]}
```

```bash theme={null}
curl "https://robodialer-service-api-9nc4t1p9.uc.gateway.dev/v1/schemas/fWxzG4nqtpHsJxS5Lm3q/required-inputs" \
  -H "Authorization: Bearer <token>"
```

```json theme={null}
{
  "schemaId": "fWxzG4nqtpHsJxS5Lm3q",
  "requiredInputs": { "fields": ["payerName", "memberId", "dateOfService", "phoneNumber"] },
  "optionalInputs": { "fields": ["memberId2", "internalCaseId"] }
}
```

Use this to confirm where the dialed number belongs: `phoneNumber` appears in
`requiredInputs.fields`, not as a separate top-level parameter. See
[Schemas](/guides/schemas#look-up-required-and-optional-inputs).

## 5. Retrieving results

The largest data-model change is the **results shape**. Legacy answers were keyed by question index.
New results are named fields defined by your schema, which removes the positional coupling.

### Legacy: GET /v1/calls/{callId}

```json theme={null}
{
  "callId": "a1B2c3D4e5F6g7H8i9J0",
  "status": "success",
  "scriptId": "123456",
  "results": {
    "0":   { "answer": false, "answerType": "boolean", "question": "Is the member active?" },
    "1":   { "answer": "Yes", "answerType": "boolean", "question": "Is prior auth required?" },
    "1.0": { "answer": "PA-90210", "answerType": "alphanumeric" }
  },
  "resultsByAlias": { "priorAuthRequired": { } },
  "highLevelOutputs": [ { "alias": "priorAuthRequired", "answer": true } ],
  "transcript": "...",
  "recordingDownloadUrl": "https://..."
}
```

### New: GET /v1/requests/{requestId}

```json theme={null}
{
  "requestId": "8bF7xK2mP9qR4sT6uV0w",
  "schemaId": "fWxzG4nqtpHsJxS5Lm3q",
  "requestType": "claim-status",
  "state": "SUCCESS",
  "results": { "claimStatus": "PAID", "paidAmount": "150.00" },
  "missingFields": [],
  "modality": "phone_only",
  "data_completeness": null,
  "error": null,
  "dateCreated": "2026-04-24T15:30:00+00:00",
  "completedAt": "2026-04-24T15:32:18+00:00",
  "transcript": "...",
  "callSummary": "...",
  "recordingDownloadUrl": "https://..."
}
```

<Note>
  Replace any code that reads `results["0"].answer` or relies on `resultsByAlias` or
  `highLevelOutputs` with code that reads named fields from `results` (for example
  `results.claimStatus`). Use `GET /v1/schemas/{schemaId}/required-inputs` and your schema
  definition to know which field names to expect.
</Note>

Two field-naming details worth knowing before you write parsers. `data_completeness` really is
snake\_case among camelCase siblings, and today it is either `"minimum"` or `null`. And `to` is
omitted from the response entirely when SuperDial resolved the number on your behalf, so treat it as
optional rather than always present. See
[Single-request vs list response shapes](/guides/concepts#single-request-vs-list-response-shapes).

### States and errors

| State        | Terminal | Meaning                                                  |
| ------------ | -------- | -------------------------------------------------------- |
| `PROCESSING` | No       | Still running. The webhook fires on completion.          |
| `SUCCESS`    | Yes      | All required fields captured. `results` fully populated. |
| `PARTIAL`    | Yes      | New. Some fields captured. See `missingFields`.          |
| `FAILURE`    | Yes      | No usable data. `results` empty, `error` populated.      |

Legacy was not binary either. Alongside `success` and `failure` it returned `processing`, plus
`cancelled` and `paused`. The mapping is `processing` to `PROCESSING`, `success` to `SUCCESS`,
`failure` to `FAILURE`. The genuinely new state to start handling is `PARTIAL`.

<Warning>
  On `PARTIAL`, `error` is **`null`**. Read `missingFields` to find out what was not captured. Only
  `FAILURE` populates the `error` object, so code that branches on `error` being non-null will treat
  every partial result as a clean success.
</Warning>

Note also that `PROCESSING` is where anything non-terminal lands, including internal error and
cancelled states. A request sitting in `PROCESSING` indefinitely is not necessarily still dialing.

When a request fails, inspect the structured error:

```json theme={null}
{
  "error": {
    "errorCategory": "NOT_FOUND",
    "errorCode": "MEMBER_NOT_FOUND",
    "errorMessage": "human-readable description"
  }
}
```

Codes you are likely to see early include `MEMBER_NOT_FOUND`, `CLAIM_NOT_FOUND`, `IVR_FAILURE`,
`CLAIM_NUMBER_MISSING`, and `MEMBER_ID_INCORRECT`.

`errorCategory` is one of exactly two values, `NOT_FOUND` or `SYSTEM_ERROR`. `errorCode` is
**open-ended**: new codes are added over time, so treat an unrecognized code as unclassified rather
than validating against a fixed list. Missing-input codes follow the `{FIELD}_MISSING` pattern and
incorrect-input codes follow `{FIELD}_INCORRECT`, which lets you auto-correct and resubmit (for
example re-prompt for a member ID) instead of treating every non-success as a dead end.

<Note>
  Do not **construct** error codes from field names. The mapping is a fixed table, not a string
  transform, and several pairs are asymmetric: the provider address field yields
  `PROVIDER_FACILITY_LOCATION_MISSING` but `PROVIDER_ADDRESS_INCORRECT`. Match on the codes you
  receive, listed in [Concepts](/guides/concepts#errorcode).
</Note>

Legacy already returned structured errors, so this is a reshaping rather than a brand-new concept.
A failed legacy call carries both a top-level `error` **string** and an `errors` object:

```json theme={null}
{
  "error": "human-readable failure reason",
  "errors": {
    "errorClass": "OTHER",
    "errorJustification": "...",
    "errorCitation": "...",
    "relevantInputKeys": "memberId\npayerName",
    "correctedPhoneNumber": "..."
  }
}
```

`errorClass` carries values such as `OTHER` and `BAD_NUMBER`. Map `errorClass` to `errorCode` and
`errorCategory`, `errorJustification` to `errorMessage`, and `relevantInputKeys` to the new
`missingFields`. The type change matters: legacy `error` is a string,
new `error` is an object. Code that logs `error` directly will start printing an object.

### Listing

`GET /v1/requests` takes `dateFrom` and `dateTo` (`YYYY-MM-DD`), and the legacy `batchId` query
parameter is renamed `requestBatchId`. With no date range and no batch ID, the default window is
**today only**.

The new endpoint also adds pagination, which legacy did not have: pass `pageSize` (default 100,
maximum 500) and `pageToken`, and follow `nextPageToken` until it is absent. See
[Pagination](/guides/reading-requests#listing-requests).

As before, transcripts and recording URLs are returned only when fetching a single request by ID,
not in the list. Note that `callSummary` **is** present in list responses, so a summary field
appearing in a list is expected rather than a sign you fetched detail.

## 6. Webhooks

Webhooks still fire on terminal states. The payload field names change, but the **signature scheme
is unchanged**, so your existing verifier keeps working.

### Configure the Request webhook URL

<Warning>
  The Requests API has its **own** webhook URL, separate from the legacy Calls API webhook. Setting
  one does nothing for the other, and there is no fallback between them. Until you set the Request
  webhook URL, terminal requests send **no webhook and no error**: delivery is a silent no-op and
  your requests will look like they simply never completed.
</Warning>

Set it before you cut production traffic over. In the SuperDial portal, go to **API**, find the
**Webhooks** section, and fill in **Request webhook**. Your existing legacy URL appears below it as
**Legacy webhook URL (/v1/calls)**, collapsed, and it keeps serving any traffic still going to
`POST /v1/calls`. Both can be set at once during a phased cutover.

To confirm delivery is working, the same panel shows **Recent deliveries (7 days)**, filterable by
Request ID. If you have submitted requests and that list is empty, the URL is not configured
correctly.

You can also pass `webhookUrl` in the body of `POST /v1/requests` to direct a single request
elsewhere. A per-request value wins over the account-level setting, and it is frozen when the
request is created: later changes to the account-level URL do not apply to requests already in
flight. Full detail in [Webhooks → Configuration](/guides/webhooks#configuration).

If neither is set, poll `GET /v1/requests/{requestId}` to get results.

### Payload

```json theme={null}
{
  "callId":     "VKW680aBcDeFgHiJkLmN",
  "status":     "success",
  "batchId":    "OEcqR7tYuI2pLm4nBv8k",
  "internalId": "asd-123"
}
```

Legacy above, new below.

```json theme={null}
{
  "requestId":      "8bF7xK2mP9qR4sT6uV0w",
  "requestBatchId": "pH9kJ2lM4nB6vC8xZ7Qr",
  "state":          "SUCCESS",
  "internalId":     "your-id",
  "internalTag":    "your-tag"
}
```

Rename `callId` to `requestId`, `batchId` to `requestBatchId`, and read `state` instead of `status`,
remembering the uppercase values and the new `PARTIAL`. Both payloads echo `internalId` and
`internalTag` only when you supplied them.

Payloads omit timestamps and results. On receipt, call `GET /v1/requests/{requestId}` to fetch the
full record. Dedupe on `requestId` (legacy: `callId`).

### Signature verification

Unchanged: HMAC-SHA256 over the raw request body, sent in the `X-Webhook-Signature` header as a
bare hex digest. The server sends the body already sorted, so you can HMAC the raw request bytes
directly and the same verifier works for both legacy and new webhooks.

The signing secret depends on which credentials created the request. Production requests use your
account `webhookSecret` if you have set one, otherwise your production API key. **Sandbox requests
use your sandbox API key and ignore `webhookSecret` entirely**, which is a common cause of signature
failures when testing.

Because the server emits the body already sorted (`json.dumps(payload, sort_keys=True)`), re-parsing
and re-serializing with `sort_keys=True` yields the same bytes as the raw request. That is why one
verifier covers both legacy and new webhooks: you only need to read the renamed fields out of the
parsed payload. Worked Python and Node examples are in
[Webhooks → Signature verification](/guides/webhooks#signature-verification).

### Delivery and retries

Delivery has two layers. On a 5xx or a connection failure we retry inline up to 3 more times (4
attempts total) with 0.5s, 1s, 2s backoff and a 10 second timeout per attempt. If those fail, the
delivery is retried later: a request gets up to 3 delivery rounds within 7 days of reaching its
terminal state.

<Note>
  Only **400 and 404** stop delivery permanently. Other 4xx responses, including 401 and 403, are
  retried across the remaining rounds, because a rotated credential can change the answer. If your
  guide or notes say "4xx is terminal", that is not how it behaves.
</Note>

Legacy webhook retry behaviour differs and does not transfer: it allows 3 attempts total, has no
inline retry ladder, and applies no send timeout.

## 7. New capabilities worth adopting

* **Modality.** [`modality`](/guides/concepts#modality) tells you how a result was obtained:
  `digital_only`, `phone_only`, `digital_plus_phone`, or `null` when nothing has been dispatched
  yet. Some requests are now satisfied digitally with no phone call at all.
* **Idempotency.** Resubmitting with the same `internalId` returns the previously created
  `requestId` instead of duplicating work. See
  [Correlation and idempotency](/guides/creating-a-request#correlation-and-idempotency).
* **Partial results.** `PARTIAL` plus `missingFields` lets you recover the fields that were captured
  instead of discarding the whole request.

## 8. Migration checklist

* [ ] **Set the Request webhook URL** in the portal under API, Webhooks, Request webhook. This is
  separate from your legacy webhook URL and silently sends nothing until configured.
* [ ] Auth is unchanged (`GET /v1/auth`, one-hour token). Reuse your existing token fetch and cache.
* [ ] Repoint endpoints: `/v1/calls` to `/v1/requests`, `/v1/calls/{id}` to `/v1/requests/{id}`,
  `/v1/scripts/...` to `/v1/schemas/...`.
* [ ] Rename request fields: `scriptId` to `schemaId`, batch wrapper `calls` to `requests`.
* [ ] **Move the dialed number** from top-level `to` into `inputs.phoneNumber`.
* [ ] Rename response fields: `callId` to `requestId`, `batchId` to `requestBatchId`, and the list
  query parameter `batchId` to `requestBatchId`.
* [ ] Rewrite result parsing: read named fields from `results` instead of question-index keys,
  `resultsByAlias`, or `highLevelOutputs`.
* [ ] Handle `PARTIAL`, and read `missingFields` on it rather than `error`, which is `null` there.
* [ ] Re-read errors from the new `error` object. It is an object, not a string, and `errorCode` is
  open-ended.
* [ ] Handle batch status codes 200, 207, 400 and 500, and read the singular per-item `error` key.
* [ ] Add pagination to any list polling: `pageSize`, `pageToken`, `nextPageToken`.
* [ ] Update your webhook handler for the new field names and `state` values. Signature
  verification needs no change. Dedupe on `requestId`.
* [ ] Validate end to end against [sandbox credentials](/guides/sandbox) before moving production
  traffic.
