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 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. On POST /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_name returns 422 with the message “Identity requires either full_name, or both first_name and last_name.”
  • If full_name and the first/last pair are all present, full_name is what identity is scored against.
  • Names inside properties (e.g. properties.first_name) are not used for identity. properties stays an arbitrary echo-back object — put the name in the top-level fields.
  • Identity carries no extra credit charge, and there is no api_version bump 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/contacts does not accept these fields.
Contacts are processed in pooled batches. If any contact in a batch has a name, the whole batch runs with identity, so a contact you submitted without a name in such a batch reports 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.
The 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. 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 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

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.