Skip to main content
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. 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

  1. POST /api/v2/contacts — submit one phone number. We charge credits per the validation tier (1 credit at the default standard) 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/v2/contacts/{contact_id}.

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 are identical — same fields, same credits, same idempotency semantics, same callbacks. Only the response differs:
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.The two new fields are null on contacts whose batch was processed without identity.
The contact.completed webhook payload gains optimaldial_tier and optimaldial_identity alongside the existing optimaldial_status, which is unchanged. Existing consumers need no action.

Validation tiers

Each contact runs at one of two tiers. Pass validation_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

Submits one phone for processing. Returns 201 with the new contact in queued status.
POST /api/v1/contacts accepts an identical request and is still supported — it just returns the legacy response shape, without the identity fields.

Request body

Response

201 Created:

Errors


Retrieve a contact

Returns the latest snapshot. Use as a fallback if you missed a webhook delivery, or to poll until status == "completed".

List contacts

Cursor-paginated, newest first. Optional status filter.

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