"Likely Answer", "Likely Voicemail") once processing finishes. Use it when your tooling enriches one row at a time and the bulk Uploads API doesn’t fit.
It’s available to every account with an API key — no special access needed. Each contact costs 1 credit at the default standard tier, or 1.5x at max — see Validation tiers.
Lifecycle
POST /api/v2/contacts— submit one phone number. We charge credits per the validation tier (1 credit at the defaultstandard) and return immediately with acontact_idinqueuedstate.- Processing happens asynchronously. Typical turnaround is within 24-48 hours.
- When processing finishes, the contact transitions to
completed(withoptimaldial_statusset) orfailed(witherror_message). - You learn the result via either:
- The
callback_urlyou provided on creation (a signed POST per contact — recommended for integrations that can listen for webhooks), or - Your registered org-level webhook endpoints (
contact.completed/contact.failed), or - Polling
GET /api/v2/contacts/{contact_id}.
- The
The Contact object
Statuses
v2 — the current version
The identity fields below come from OptimalDial Identity, currently in public
beta. The response shape is stable; we’re still tuning the thresholds. Feedback
welcome at support@optimaldial.com.
/api/v2/contacts mirrors every v1 route — create, list, and retrieve — and
additionally reports the nine-tier OptimalDial_Status and the
Identity band.
Requests share the same fields, credits, idempotency semantics, and callbacks.
v2 additionally accepts optional
full_name / first_name / last_name fields
that turn on identity for the contact — see
Requesting Identity. The response differs as follows:
Requesting Identity
Identity needs one extra input: the contact’s name. OnPOST /api/v2/contacts,
supply full_name, or both first_name and last_name. Each is a
string of up to 200 characters; surrounding whitespace is trimmed and a blank
value counts as absent.
- Supplying only one of
first_name/last_namereturns422with the message “Identity requires eitherfull_name, or bothfirst_nameandlast_name.” - If
full_nameand the first/last pair are all present,full_nameis what identity is scored against. - Names inside
properties(e.g.properties.first_name) are not used for identity.propertiesstays an arbitrary echo-back object — put the name in the top-level fields. - Identity carries no extra credit charge, and there is no
api_versionbump on webhooks — the new fields are additive. - On an idempotent replay (same
idempotency_key), the existing contact is returned and any name fields on the replay are ignored. POST /api/v1/contactsdoes not accept these fields.
optimaldial_identity: "Unknown" (no signal) and an
even-numbered tier rather than null. A contact in a batch where nobody
supplied a name reports optimaldial_identity: null. Either way, Unknown is
not the same as “safe” — treat it as unknown.
v1 is legacy — supported indefinitely, with no removal planned. It simply
doesn’t include identity:
optimaldial_status there keeps returning the Answer
Intent value, so existing filters keep working exactly as before.There is no migration. Both versions operate on the same contacts, so one
created on v1 can be read on v2 and vice versa. Point your calls at /api/v2/
whenever you’re ready — or don’t, and stay on v1.optimaldial_answer_intent and optimaldial_identity are null on contacts
whose batch was processed without identity — i.e. no contact in it supplied a
name.contact.completed / contact.failed webhook payloads gain
optimaldial_tier and optimaldial_identity, plus the echoed full_name /
first_name / last_name, alongside the existing optimaldial_status,
which is unchanged. No api_version bump; existing consumers need no action.
Validation tiers
Each contact runs at one of two tiers. Passvalidation_tier on Create a contact; it defaults to standard.
The value is case-insensitive, and anything other than
standard or max returns 400.
max is available on this endpoint at any volume, including a single contact. The 100-number minimum documented on the bulk Uploads API applies only there — if your pipeline produces a handful of numbers at a time, submit them here per-row rather than accumulating to reach that floor.
How max is billed per contact
Credits are whole-number ledger entries, so max can’t charge a fraction on a single-contact request. It bills marginally instead: successive max contacts are charged 2, 1, 2, 1, … credits.
The alternation runs across your current queued batch — the max contacts you’ve submitted that haven’t moved to processing yet. Whatever that batch ends up holding, its total is exactly ceil(N × 1.5): the same price as submitting those N numbers as one max upload.
So there’s no cost penalty for submitting one at a time instead of batching.
Because the charge alternates, the subtotal for any one run depends on where it lands in the batch: five
max contacts cost 8 credits when they open a fresh batch, or 7 when they land mid-alternation. A run of n is never charged more than ceil(n × 1.5). Read credits_charged on each response for the exact amount deducted rather than assuming a fixed per-contact cost.
Create a contact
201 with the new contact in queued status.
POST /api/v1/contacts accepts the same request minus the full_name /
first_name / last_name fields and is still supported — it returns the
legacy response shape, without the identity fields.Request body
- cURL
- Node.js
- Python
Response
201 Created:
Errors
Retrieve a contact
status == "completed".
List contacts
status filter.
Result delivery
When a contact reachescompleted or failed, OptimalDial fires up to two notifications:
- Per-contact callback — if you provided
callback_urlon creation, we POST a signed JSON body to it. Same signature scheme and retry semantics as org-level webhooks: HMAC-SHA256 off"{ts}.{body}"using yourcallback_secret, 6 attempts with exponential backoff. Headers includeX-OptimalDial-SignatureandX-OptimalDial-Timestamp. - Org-level webhooks — your registered webhook endpoints receive
contact.completed/contact.failedevents for every contact that finishes. Useful for fan-out to internal systems independent of the per-contact callback.
properties object on the result echoes back exactly what you sent on creation — use it to correlate the result with your own row IDs without storing the OptimalDial contact_id.