Skip to main content

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

  1. POST /api/v1/contacts — submit one phone number. We charge 1 credit and return immediately with a contact_id in queued state.
  2. Processing happens asynchronously. Typical turnaround is within 24-48 hours.
  3. When processing finishes, the contact transitions to completed (with optimaldial_status set) or failed (with error_message).
  4. 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}.

The Contact object

FieldTypeNotes
idstring (UUID)Stable identifier.
organization_idstring (UUID)Org the contact belongs to.
phonestringE.164-normalized phone (US/CA only).
propertiesobjectWhatever extra fields you sent on creation, echoed back in result payloads.
statusenumOne of: queued, processing, completed, failed, discarded.
optimaldial_statusstring | nullThe classification — e.g. "Likely Answer". Populated when status == "completed".
error_messagestring | nullPopulated when status == "failed".
credits_chargedintegerAlways 1. Refunded on cancel.
created_atstring (ISO 8601)UTC timestamp.
updated_atstring (ISO 8601)UTC timestamp of the last status change.

Statuses

StatusMeaning
queuedSubmitted, awaiting processing.
processingProcessing in progress.
completedResult is on optimaldial_status.
failedProcessing finished but did not produce a result; see error_message.
discardedDiscarded by OptimalDial; credit refunded. Rare.

Create a contact

POST /api/v1/contacts
Submits one phone for processing. Returns 201 with the new contact in queued status.

Request body

FieldTypeRequiredNotes
phonestringyesAny common format. Normalized to E.164 server-side. Must be US or Canadian.
propertiesobjectnoArbitrary extra fields echoed back in result payloads. Use these to correlate the result with your own row IDs.
callback_urlstring (HTTPS)noOptional 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_secretstringnoHMAC-SHA256 secret used to sign the callback_url payload.
idempotency_keystringnoOptional. 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"
  }'

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

StatusWhen
401Missing, invalid, or revoked API key
402Organization out of credits
403Enterprise access not enabled — contact support
422Phone is unparseable or not US/CA
429Rate limit hit

Retrieve a contact

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"

List contacts

GET /api/v1/contacts
Cursor-paginated, newest first. Optional status filter.
ParamTypeDefaultNotes
statusenumFilter by one of the statuses.
limitinteger (1–100)50Page size.
cursorstringPass 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:
  1. 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.
  2. 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.