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

# Creating a Request

> Submit a single request or a batch of requests with POST /v1/requests.

`POST /v1/requests` creates one or more [requests](/guides/concepts#request) and returns their IDs. The body shape varies based on whether you're submitting a single item or a batch.

## Authentication

Protected routes require a bearer token, including `POST /v1/requests` and the other `/v1/requests*` endpoints. Get one with `GET /v1/auth` (passing your API key and secret as the `Robodialer-API-Key` and `Robodialer-API-Secret` headers). Pass the returned token as `Authorization: Bearer <token>` on every subsequent HTTP request.

Tokens are valid for **1 hour**. Refresh by calling `GET /v1/auth` again; there's no refresh-token flow. For long-running batches or background workers, fetch a fresh token at the start of each work cycle, or refresh on `401` responses.

```python theme={null}
import requests

def fetch_token(api_key: str, api_secret: str) -> str:
    r = requests.get(
        "https://robodialer-service-api-9nc4t1p9.uc.gateway.dev/v1/auth",
        headers={
            "Robodialer-API-Key": api_key,
            "Robodialer-API-Secret": api_secret,
        },
    )
    r.raise_for_status()
    return r.json()["token"]
```

## Single request

Submit a single request by sending a JSON body with the request's fields directly:

```bash theme={null}
curl -X POST https://robodialer-service-api-9nc4t1p9.uc.gateway.dev/v1/requests \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "schemaId": "fWxzG4nqtpHsJxS5Lm3q",
    "inputs": {
      "payerName": "Sample Insurance Co",
      "memberId": "TEST123456789",
      "phoneNumber": "2125551234",
      "providerNpi": "1234567890",
      "dateOfService": "2026-03-15"
    },
    "internalId": "claim_internal_456"
  }'
```

### Response

```json theme={null}
{
  "requestId": "8bF7xK2mP9qR4sT6uV0w",
  "requestBatchId": "pH9kJ2lM4nB6vC8xZ7Qr",
  "internalId": "claim_internal_456"
}
```

If your account is enabled for [**payer phone number lookup**](/guides/payer-resolution) (opt-in: ask your account team), the response carries a `payerLookup` sub-object describing the payer/phone lookup for the request.

```json theme={null}
{
  "requestId": "8bF7xK2mP9qR4sT6uV0w",
  "requestBatchId": "pH9kJ2lM4nB6vC8xZ7Qr",
  "internalId": "claim_internal_456",
  "payerLookup": {
    "inputPayerName": "Sample Insurance Co",
    "inputPhoneNumber": null,
    "matchedPayerName": "Sample Insurance Company, Inc.",
    "matchedPayerPhone": "8005551234",
    "phoneNumberToUse": "8005551234",
    "phoneNumberSource": "superdial"
  }
}
```

`payerLookup` is always present on the response and reports the number that will be dialed (`phoneNumberToUse`) and where it came from (`phoneNumberSource`: `superdial`, `phoneBook`, or `input`). The `matched*` fields are populated only when SuperDial dials its own matched number; if your supplied `phoneNumber` is dialed, they're `null` and `phoneNumberSource` is `"input"`. By default a supplied `phoneNumber` always wins: set [`useMatchedPayerPhone: true`](/guides/payer-resolution#choosing-which-number-to-dial) to dial the matched number instead. The same block is returned on [reads](/guides/reading-requests). See the [Payer Phone Number Lookup guide](/guides/payer-resolution) for the full field reference.

The request begins running immediately. Configure a [webhook](/guides/webhooks) to be notified when it reaches a terminal state, then call [`GET /v1/requests/{requestId}`](/guides/reading-requests#single-request) to fetch the full result.

## Batch

Submit multiple requests in one call by wrapping them in `{ "requests": [...] }`. The response returns one entry per submitted request, in the same order.

**Each entry carries its own `requestBatchId`, and those IDs may or may not match across the batch.** Within a single submission, entries scheduled for the same business day share a `requestBatchId`; entries scheduled for different days get distinct ones. Whether your batch lands on a single day or spills across multiple days depends on your account's daily call capacity and any work already pending from prior submissions on those days. Always read each entry's `requestBatchId` from the response. Don't assume they match.

```bash theme={null}
curl -X POST https://robodialer-service-api-9nc4t1p9.uc.gateway.dev/v1/requests \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "requests": [
      {
        "schemaId": "fWxzG4nqtpHsJxS5Lm3q",
        "inputs": { "payerName": "Sample Insurance Co", "memberId": "TEST123456789", "phoneNumber": "2125551234" },
        "internalId": "claim_001"
      },
      {
        "schemaId": "fWxzG4nqtpHsJxS5Lm3q",
        "inputs": { "payerName": "Sample Insurance Co", "memberId": "TEST987654321", "phoneNumber": "2125551234" },
        "internalId": "claim_002"
      }
    ]
  }'
```

### Batch response

A batch responds with one entry per request, in the same order as the input. Each entry is either a success body or the uniform `{error, message, [details]}` envelope:

```json theme={null}
{
  "requests": [
    {
      "requestId": "8bF7xK2mP9qR4sT6uV0w",
      "requestBatchId": "pH9kJ2lM4nB6vC8xZ7Qr",
      "internalId": "claim_001"
    },
    {
      "error": "INVALID_REQUEST",
      "message": "schemaId is required"
    }
  ]
}
```

The HTTP status code reflects the aggregate outcome:

| Status code | Meaning |
| - | - |
| `200` | All entries in the batch were created successfully. |
| `207` | Mixed: at least one entry succeeded and at least one failed with a **client-side (4xx)** error. Inspect each entry. |
| `400` | Either the top-level request body was invalid (e.g., `requests` was empty), OR every entry in the batch failed with a client-side error. |
| `500` | An internal failure occurred during batch processing. Body is **either** the top-level `{error: "INTERNAL_ERROR", message: "..."}` envelope (the common case) **or** `{"requests": [...]}` with per-entry envelopes. Check for the `requests` key to tell them apart, and treat each entry independently. |

<Tip>
  Batches don't have to be all-or-nothing. We recommend treating each entry independently: successful entries are real, persisted requests, even if other entries in the same batch failed.
</Tip>

<Note>
  To list every request under a given `requestBatchId` later, for example to track progress on one day's slice of a large batch, use [`GET /v1/requests?requestBatchId=...`](/guides/reading-requests#by-batch). For a multi-day batch, iterate over the unique IDs returned in the create response.
</Note>

## Required fields

Every request body must include:

| Field | Description |
| - | - |
| `schemaId` | The [schema](/guides/concepts#schema) that defines the output fields. Discover yours via [`GET /v1/schemas`](/guides/schemas). |
| `inputs` | A JSON object of `{ key: value }` pairs. **All values must be strings**, including fields that hold several values: see [Multi-value inputs](#multi-value-inputs). The keys are schema-specific: discover required and optional ones via [`GET /v1/schemas/{schemaId}/required-inputs`](/guides/schemas#look-up-required-and-optional-inputs). Typical examples include `payerName`, `memberId`, `phoneNumber`, `providerNpi`, `dateOfService`. Those two lists are the baseline, not a limit: you can also send [extra keys of your own](#extra-inputs). |

`requestType` is **server-derived** from the schema: you'll see the canonical value on the `RequestResponse` returned by `GET /v1/requests/{requestId}`. You can still send a `requestType` field on the POST body if existing client code does; it's silently ignored.

Required `inputs.{key}` fields are validated when the request is created: if any are missing or fail format checks, the request is rejected with HTTP 400 and the uniform `INVALID_INPUTS` error envelope:

```json theme={null}
{
  "error": "INVALID_INPUTS",
  "message": "Required inputs are missing or invalid.",
  "details": {
    "missingInputs": ["memberId", "providerNpi"],
    "invalidInputs": { "dateOfService": "must be YYYY-MM-DD" }
  }
}
```

`details` is only present on this `INVALID_INPUTS` path; other 400 errors carry just `error` + `message`. Discover the required `inputs` per schema with [`GET /v1/schemas/{schemaId}/required-inputs`](/api-reference/schemas/required-inputs-for-a-schema), or ask your account team. See the [API Reference for `POST /v1/requests`](/api-reference/requests/create-a-request) for the full request body schema.

<Note>
  If your account is enabled for [**per-payer required inputs**](/guides/payer-required-inputs) (opt-in: ask your account team), some payers require additional inputs beyond the schema's fields, depending on `payerName`. A missing one is reported here in `details.missingInputs` like any other. Add it and retry.
</Note>

## Multi-value inputs

Send a field that holds several values as **one comma-delimited string**, not a JSON array. Every value in `inputs` must be a string, and the type check runs before anything else, so an array is rejected on arrival.

```json theme={null}
// Correct
{
  "schemaId": "fWxzG4nqtpHsJxS5Lm3q",
  "inputs": {
    "cptCodes": "99213,99214",
    "icd10Codes": "E11.9,I10"
  }
}
```

```json theme={null}
// Rejected: HTTP 400
{
  "schemaId": "fWxzG4nqtpHsJxS5Lm3q",
  "inputs": {
    "cptCodes": ["99213", "99214"],
    "icd10Codes": ["E11.9", "I10"]
  }
}
```

```json theme={null}
// The 400 names every offending field
{
  "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)"
    }
  }
}
```

Nothing validates the separator: the string is handed to the voice agent as you sent it, and a comma-delimited list reads naturally on the call. A space after each comma is fine. The same rule covers any field you use this way, `cptCodes` and `icd10Codes` being the common ones.

<Warning>
  **Moving from the legacy Calls API?** This is the difference most likely to catch you. `POST /v1/calls` accepts an array for a field like `cptCodes`; `POST /v1/requests` does not. A mapping that has worked for months against `/v1/calls` will fail on **every** request here, and because a 400 creates nothing, no request appears in the portal to tell you so. See [Migrating from the Calls API](/guides/api-migration#input-values-must-be-strings).
</Warning>

## Optional fields

| Field | Description |
| - | - |
| `webhookUrl` | Per-request [webhook](/guides/webhooks) URL override. Wins over the account default for this one request. |
| `useMatchedPayerPhone` | Boolean (default `false`). When `true` and [payer phone number lookup](/guides/payer-resolution) is enabled, dial SuperDial's matched payer number even if you supplied `phoneNumber`, falling back to your number when no match is found. Ignored when lookup is off. Goes here at the top level of the request, not inside `inputs` — see [where to put the flag](/guides/payer-resolution#where-to-put-the-flag). |
| `internalId` | **Optional** correlation ID, and also the request's idempotency key. Echoed on all reads and webhooks. If you supply one it must be **unique per request** (see below). If you omit it, SuperDial generates one for you. |
| `internalTag` | Opaque tag echoed on reads and webhooks (e.g. `"march-batch"`). |
| `aiOnly` | Boolean. When `true`, any phone call this request makes is handled only by our automated voice agent, never a human agent. Leaving it `false` adds no restriction: it doesn't force human-agent handling. Defaults to `false`. |
| `onshoreOnly` | Boolean. When `true`, all phone handling for this request is restricted to US-based agents. Leaving it `false` adds no restriction. Defaults to `false`. |

## Extra inputs

**The keys of `inputs` aren't limited to the ones your schema declares.** Any additional key you send is accepted, stored with the request, echoed back on every read and webhook, and given to the voice agent as context on any phone call the request makes. Nothing rejects a key just because [`GET /v1/schemas/{schemaId}/required-inputs`](/guides/schemas#look-up-required-and-optional-inputs) didn't list it.

Use this for context that helps the call but that the schema never asked to collect: why the claim was denied last time, a deadline the representative should hear about, which department has already turned you away.

```json theme={null}
{
  "schemaId": "fWxzG4nqtpHsJxS5Lm3q",
  "inputs": {
    "payerName": "Sample Insurance Co",
    "memberId": "TEST123456789",
    "phoneNumber": "2125551234",
    "providerNpi": "1234567890",
    "dateOfService": "2026-03-15",

    // extra keys, in neither requiredInputs nor optionalInputs
    "priorDenialReason": "CO-197 no authorization on file",
    "appealDeadline": "2026-04-01"
  },
  "internalId": "claim_internal_456"
}
```

### Rules that still apply

An extra key is not a way around validation. The same checks run on it as on a schema field:

| Rule | Detail |
| - | - |
| Values must be strings | Each value is a string or `null`, exactly as for schema fields. A nested object or array is rejected with `INVALID_INPUTS` and a `details.invalidInputs` entry such as `priorDenialReason must be a string (got dict)`. Flatten the value, or put several values in one comma-delimited string: see [Multi-value inputs](#multi-value-inputs). |
| The universal content rules | The [scientific-notation and curly-brace rules](/guides/input-validation#universal-value-rules) apply to every non-empty value, extra keys included. |

An extra key is never reported in `details.missingInputs`, because nothing requires it. Leave it out on some requests and send it on others, as you like.

### What extra inputs don't do

Extra inputs are context, not instructions. They don't add questions to the call: the agent won't ask a payer about a key you invented, and the key won't come back in the request's `results`. What gets asked, and what gets returned, are both fixed by the [schema](/guides/concepts#schema). If you need the agent to *ask* something new, the schema itself has to change. Talk to your account team.

<Warning>
  **Two rules change what the agent actually sees.** A key whose name contains `internal` (case-insensitive) is withheld from the agent entirely, so an `internalNote` is stored and echoed back but never reaches the call; for data you only want correlated, use the top-level `internalId` and `internalTag` instead. And values are reformatted so they can be spoken aloud: keys whose name contains `name`, `date`, or `address` get title-casing, underscores as spaces, or a long-form spoken date, and **any value of more than two words is title-cased whatever its key is called.** The `priorDenialReason` above reaches the agent as `Co-197 No Authorization On File`. Keep an extra value short if its capitalization matters, and don't name an extra key `...Name` unless it holds a person's or organization's name.
</Warning>

<Note>
  **Send what helps the call, not your whole record.** Every extra key is put in front of the agent on every turn of the conversation, so a long tail of irrelevant identifiers gives it more chances to offer a representative something beside the point. Two or three well-chosen fields beat twenty.
</Note>

## Correlation and idempotency

`internalId` is **optional** and serves two purposes:

1. **Correlation**: when you supply one, it's echoed back on every read and every webhook for that request, so you can tie SuperDial results to your own records.
2. **Idempotency**: `internalId` *is* the idempotency key. A `POST /v1/requests` that reuses an `internalId` you've sent before does **not** create a new request; it returns the originally created `requestId` (and the original `payerLookup`, if any).

<Warning>
  Because `internalId` is the idempotency key, **a new `internalId` must be different on every distinct request you want to place.** Reusing one is not an error and produces no warning: it silently returns the original request instead of starting new work. Use a value that's naturally unique per request (a UUID, or your own primary key), not a constant or a reused batch label.
</Warning>

**You don't have to send one.** `internalId` is optional. If you omit it, SuperDial generates and maintains its own unique idempotency key server-side, so retries are still de-duplicated for you. Supply your own only when you want to correlate results back to records on your end; otherwise leave it off and let SuperDial manage it.

```json theme={null}
// First call
POST /v1/requests  { "internalId": "claim_001", ... }
→ 200  { "requestId": "8bF7xK2mP9qR4sT6uV0w", ... }

// Retry with the SAME internalId: no new request is created
POST /v1/requests  { "internalId": "claim_001", ... }
→ 200  { "requestId": "8bF7xK2mP9qR4sT6uV0w", ... }   // same ID, no duplicate

// Omit internalId entirely: SuperDial generates its own key
POST /v1/requests  { ... }            // no internalId
→ 200  { "requestId": "aC3hN5jD8eL1fM2gK6Yo", ... }   // new request every call
```

`internalId`-based idempotency applies on a **per-request** basis. In batch submissions, each entry with an `internalId` is independently deduped: a duplicate entry returns the previously created `requestId` and `requestBatchId` rather than creating a new request. There is no batch-level idempotency key, only per-entry.

## What happens after creation

The request immediately begins running through one or more [modalities](/guides/concepts#modality) (electronic systems and/or phone calls). Completion time varies: electronic-only requests can finish quickly; phone-backed requests depend on payer responsiveness and hold times.

Configure a [webhook](/guides/webhooks) to be notified the moment a request finishes, then call [`GET /v1/requests/{requestId}`](/guides/reading-requests#single-request) to fetch the full result.

## Error handling

Every non-2xx response from `/v1/requests` returns the same envelope:

```json theme={null}
{
  "error": "MACHINE_CODE",
  "message": "Human-readable description.",
  "details": { /* optional, only on INVALID_INPUTS */ }
}
```

Switch on `error` for programmatic dispatch; surface `message` to humans.

| HTTP code | `error` code | Meaning | What to do |
| - | - | - | - |
| `400` | `INVALID_REQUEST` | Body wasn't a JSON object, a required field is missing (`schemaId`, `inputs`), the batch `requests` array is empty, or scheduling capacity is exceeded. The validator's specific reason is in `message`. | Fix the body and retry |
| `400` | `INVALID_INPUTS` | Input validation failed. `details.missingInputs` lists field names that weren't supplied; `details.invalidInputs` maps field name to the reason it was rejected. With [per-payer required inputs](/guides/payer-required-inputs) enabled, some payers add inputs that can appear here too. | Fix the inputs and retry |
| `400` | `PAYER_NOT_FOUND` | Payer phone number lookup path: the supplied `payerName` did not match any known payer. Only fires when [lookup is enabled for your account](/guides/payer-resolution), `payerName` is set, and `phoneNumber` is not. | Confirm spelling, try a more canonical payer name, or supply `phoneNumber` directly to skip the lookup |
| `401` | (gateway envelope) | Bearer token missing, malformed, or expired. Returns the gateway's `{code, message}` shape, not the envelope above. | Fetch a fresh token via `GET /v1/auth` |
| `404` | `SCHEMA_NOT_FOUND` | `schemaId` doesn't exist for your account, or has been retired. On a batch, schema-not-found surfaces per-entry as `INVALID_REQUEST` with `"Schema not found"` in `message`. Read `message` to distinguish 4xx causes when an entry has no status code. | Discover valid `schemaId` values with [`GET /v1/schemas`](/api-reference/schemas/list-schemas) |
| `404` | `ACCOUNT_NOT_FOUND` | The API key resolves to an account that no longer exists. Rare: wrong-API-key cases hit `INVALID_API_KEY` at `/v1/auth` first. | Contact support |
| `500` | `INTERNAL_ERROR` | Unexpected server failure. **Single-item POST:** body is the envelope above. **Batch:** body can take either shape: the top-level `INTERNAL_ERROR` envelope (most common) or `{"requests": [...]}` with per-entry envelopes (server-error entries surface here as `INVALID_REQUEST` rather than `INTERNAL_ERROR`). Check for the `requests` key to tell the two shapes apart. | Retry once; escalate if persistent |
| `500` | `PAYER_LOOKUP_FAILURE` | Payer phone number lookup path: the supplied `payerName` matched a payer but our database has no phone number on file, or the lookup hit a transient infra failure. Only fires when [lookup is enabled for your account](/guides/payer-resolution). | Retry with backoff, or supply `phoneNumber` directly to skip the lookup |

For batch submissions, **each failed entry inside the `requests` array carries the same envelope shape** as a top-level error, so you can use the same error handler everywhere.


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