Documentation Index
Fetch the complete documentation index at: https://docs.optimaldial.com/llms.txt
Use this file to discover all available pages before exploring further.
The Contacts API accepts one phone number per request and returns an OptimalDial Status classification (e.g. "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.
This endpoint is Enterprise-only. To enable access for your organization, email [email protected]. Until access is granted, every call returns 403 with error: "feature_not_enabled".
Lifecycle
POST /api/v1/contacts — submit one phone number. We charge 1 credit and return immediately with a contact_id in queued state.
- Processing happens asynchronously. Typical turnaround is within 24-48 hours.
- When processing finishes, the contact transitions to
completed (with optimaldial_status set) or failed (with error_message).
- You learn the result via either:
- The
callback_url you 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/v1/contacts/{contact_id}.
| Field | Type | Notes |
|---|
id | string (UUID) | Stable identifier. |
organization_id | string (UUID) | Org the contact belongs to. |
phone | string | E.164-normalized phone (US/CA only). |
properties | object | Whatever extra fields you sent on creation, echoed back in result payloads. |
status | enum | One of: queued, processing, completed, failed, discarded. |
optimaldial_status | string | null | The classification — e.g. "Likely Answer". Populated when status == "completed". |
error_message | string | null | Populated when status == "failed". |
credits_charged | integer | Always 1. Refunded on cancel. |
created_at | string (ISO 8601) | UTC timestamp. |
updated_at | string (ISO 8601) | UTC timestamp of the last status change. |
Statuses
| Status | Meaning |
|---|
queued | Submitted, awaiting processing. |
processing | Processing in progress. |
completed | Result is on optimaldial_status. |
failed | Processing finished but did not produce a result; see error_message. |
discarded | Discarded by OptimalDial; credit refunded. Rare. |
Submits one phone for processing. Returns 201 with the new contact in queued status.
Request body
| Field | Type | Required | Notes |
|---|
phone | string | yes | Any common format. Normalized to E.164 server-side. Must be US or Canadian. |
properties | object | no | Arbitrary extra fields echoed back in result payloads. Use these to correlate the result with your own row IDs. |
callback_url | string (HTTPS) | no | Optional per-contact webhook. We POST to it when the contact’s result is ready. Signed with callback_secret if provided. Same retry semantics as registered org webhooks (6 attempts with exponential backoff). |
callback_secret | string | no | HMAC-SHA256 secret used to sign the callback_url payload. |
idempotency_key | string | no | Optional. Re-posting with the same key returns the existing contact without an extra credit charge. Scoped per (organization, api_key). |
curl -X POST https://api.optimaldial.com/api/v1/contacts \
-H "Authorization: Bearer $OPTIMALDIAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+15551234567",
"properties": { "row_id": "abc123", "first_name": "Alex" },
"callback_url": "https://hooks.example.com/optimaldial-contact",
"callback_secret": "shared-secret-for-hmac",
"idempotency_key": "row-abc123"
}'
const res = await fetch("https://api.optimaldial.com/api/v1/contacts", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPTIMALDIAL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
phone: "+15551234567",
properties: { row_id: "abc123", first_name: "Alex" },
callback_url: "https://hooks.example.com/optimaldial-contact",
callback_secret: process.env.CALLBACK_SECRET,
idempotency_key: "row-abc123",
}),
});
const contact = await res.json();
resp = requests.post(
"https://api.optimaldial.com/api/v1/contacts",
headers={
"Authorization": f"Bearer {os.environ['OPTIMALDIAL_API_KEY']}",
"Content-Type": "application/json",
},
json={
"phone": "+15551234567",
"properties": {"row_id": "abc123", "first_name": "Alex"},
"callback_url": "https://hooks.example.com/optimaldial-contact",
"callback_secret": os.environ["CALLBACK_SECRET"],
"idempotency_key": "row-abc123",
},
)
contact = resp.json()
Response
201 Created:
{
"id": "9b1f4f5e-0a8a-4c4c-8d2e-d9d0f9b4a1f1",
"organization_id": "...",
"phone": "+15551234567",
"properties": { "row_id": "abc123", "first_name": "Alex" },
"status": "queued",
"optimaldial_status": null,
"error_message": null,
"credits_charged": 1,
"created_at": "2026-05-04T18:01:23+00:00",
"updated_at": "2026-05-04T18:01:23+00:00"
}
Errors
| Status | When |
|---|
401 | Missing, invalid, or revoked API key |
402 | Organization out of credits |
403 | Enterprise access not enabled — contact support |
422 | Phone is unparseable or not US/CA |
429 | Rate limit hit |
GET /api/v1/contacts/{contact_id}
Returns the latest snapshot. Use as a fallback if you missed a webhook delivery, or to poll until status == "completed".
curl "https://api.optimaldial.com/api/v1/contacts/$CONTACT_ID" \
-H "Authorization: Bearer $OPTIMALDIAL_API_KEY"
Cursor-paginated, newest first. Optional status filter.
| Param | Type | Default | Notes |
|---|
status | enum | — | Filter by one of the statuses. |
limit | integer (1–100) | 50 | Page size. |
cursor | string | — | Pass back next_cursor from the previous response. |
curl "https://api.optimaldial.com/api/v1/contacts?status=completed&limit=100" \
-H "Authorization: Bearer $OPTIMALDIAL_API_KEY"
{
"data": [ /* Contact, Contact, … */ ],
"next_cursor": "2026-05-04T17:55:00+00:00"
}
Result delivery
When a contact reaches completed or failed, OptimalDial fires up to two notifications:
- Per-contact callback — if you provided
callback_url on creation, we POST a signed JSON body to it. Same signature scheme and retry semantics as org-level webhooks: HMAC-SHA256 of f"{ts}.{body}" using your callback_secret, 6 attempts with exponential backoff. Headers include X-OptimalDial-Signature and X-OptimalDial-Timestamp.
- Org-level webhooks — your registered webhook endpoints receive
contact.completed / contact.failed events for every contact that finishes. Useful for fan-out to internal systems independent of the per-contact callback.
Both delivery paths emit the same payload shape, wrapping the Contact object:
{
"id": "evt_contact_...",
"type": "contact.completed",
"created_at": "2026-05-04T19:30:01+00:00",
"data": {
"contact": {
"id": "9b1f4f5e-0a8a-4c4c-8d2e-d9d0f9b4a1f1",
"phone": "+15551234567",
"properties": { "row_id": "abc123", "first_name": "Alex" },
"status": "completed",
"optimaldial_status": "Likely Answer",
"created_at": "2026-05-04T18:01:23+00:00"
}
}
}
The 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.