> ## 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` |
| Discover output fields | no equivalent: you read whatever keys came back | `GET /v1/schemas/{schemaId}` |
| List definitions | `GET /v1/scripts` | `GET /v1/schemas` |
| Authentication | `GET /v1/auth` | `GET /v1/auth` (same endpoint) |
| Dialed number | top-level `to` | `inputs.phoneNumber` |
| `inputs` value types | any JSON type where the field has no format rule | strings (or `null`) only |
| 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.

### Input values must be strings

`POST /v1/requests` rejects any non-string value in `inputs` before it reads anything else. `POST /v1/calls` has no such check, so a field you have always sent as an array keeps working on the legacy endpoint and fails on **every** request to the new one.

Multi-value fields are the usual case. Join them into a comma-delimited string:

| Field | Legacy Calls API accepted | Requests API needs |
| - | - | - |
| `cptCodes` | `["99213", "99214"]` | `"99213,99214"` |
| `icd10Codes` | `["E11.9", "I10"]` | `"E11.9,I10"` |

The rejection names every offending field, so one response tells you the whole fix:

```json theme={null}
{
  "error": "INVALID_INPUTS",
  "message": "Required inputs are missing or invalid.",
  "details": {
    "invalidInputs": {
      "cptCodes": "cptCodes must be a string (got list)",
      "icd10Codes": "icd10Codes must be a string (got list)"
    }
  }
}
```

<Warning>
  A 400 creates nothing: no request, no batch, no call, nothing in the portal. An integration that
  posts and never reads the response body looks like it is submitting work when every submission is
  being refused. Log `details.invalidInputs` on the first 400 before you retry. See
  [Multi-value inputs](/guides/creating-a-request#multi-value-inputs).
</Warning>

### Other format differences

The per-field format rules themselves are shared. Both APIs run the same validator, so the phone, date, dollar-amount, boolean and custom-pattern rules in [Input Validation](/guides/input-validation) behave identically. What differs is the shape of the body and how each API treats absent or placeholder values:

| Behaviour | Legacy (Calls API) | New (Requests API) |
| - | - | - |
| Non-string `inputs` values | No type gate. An array or number is accepted for any field that carries no configured format rule, and stringified downstream. | Rejected up front: `{field} must be a string (got list)`. |
| Script default values | Defaults configured on the script fill an input you omit or send empty. | Not applied at validation. An omitted required input is reported in `details.missingInputs`. |
| Placeholder values | `"-"`, `"N/A"`, `"n/a"`, `"unknown"` count as empty, so a script default can replace them. | Treated as real values. They pass the empty check and go to the call. With [member-ID validation](/guides/input-validation#member-id-validation) on, `"N/A"` in `memberId` is rejected. |
| Agent routing | `agentAssignment: "ai"` and `agentLocation: "us"` or `"any"`, both strings | `aiOnly` and `onshoreOnly`, both JSON booleans. A string `"true"` returns `400 INVALID_REQUEST`. |
| `internalId` | Echoed back only. A repeat creates a second call. | Idempotency key. A repeat returns the original `requestId`. |
| Unknown top-level keys | Ignored | Ignored. `to` and `requestType` are among them, so neither errors when you leave it in place. |

<Note>
  The placeholder difference is easy to overlook. If your export writes `"N/A"` into an optional
  field, the legacy API treated it as blank; the Requests API sends the literal string to the
  representative on the call. Send an empty string, or omit the key.
</Note>

### 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`). To find out which field names to expect, call
  [`GET /v1/schemas/{schemaId}`](/api-reference/schemas/retrieve-a-schema). Its `resultsFields`
  lists every field the schema can return, so you can write the mapping before you run a request.
  See [Schemas → Look up the fields a schema returns](/guides/schemas#look-up-the-fields-a-schema-returns).
</Note>

<Warning>
  If your account has Title Case trace fields turned on for the legacy API, your existing keys do
  not carry over. That setting rewrites the keys of `traceResults.traceFields` on `/v1/calls`. It
  does not apply to `results` on `/v1/requests`, and the names from
  `GET /v1/schemas/{schemaId}` match `results`. Map the new names rather than reusing your Title
  Case keys.
</Warning>

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`.
* [ ] **Convert every array-valued input to a comma-delimited string** (`cptCodes`, `icd10Codes`,
  and anything else you send as a list). Arrays are rejected on every request.
* [ ] Check what your script defaults used to fill in. Defaults are not applied on the Requests
  API, so send those fields explicitly.
* [ ] Replace `"N/A"`, `"-"` and `"unknown"` placeholders with an empty string or an omitted key.
* [ ] Swap `agentAssignment` / `agentLocation` strings for the `aiOnly` / `onshoreOnly` booleans.
* [ ] 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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.