Skip to main content
The Uploads API is the heart of OptimalDial. You submit a list of phone numbers — as CSV or JSON — and receive a typed Upload resource that you can poll for status or pull processed results from.

The Upload object

Every endpoint in this section returns or includes an Upload. Most fields are populated incrementally as the upload moves through its lifecycle.

result_summary

Populated once the upload is completed. Lets you see the shape of a list without downloading it.
format is "identity" when the list was processed with name columns mapped and "answer_intent" otherwise. It tells you which vocabulary status_counts keys — and which values statuses accepts on the download endpoint.

Upload statuses

The pending_mapping, validating, validation_failed, and awaiting_confirmation statuses are only reachable from the in-app upload flow; API-created uploads jump straight to ready_for_processing.

OptimalDial Identity

OptimalDial Identity is in public beta. It is available to every account and the output format is stable, but we’re still tuning the thresholds against real call outcomes. We’d genuinely like your feedback — especially how the identity bands compare to what your reps report from the field. Get in touch at support@optimaldial.com.
Every number gets scored on two independent questions:
  • Answer Intent — will this number pick up?
  • Identity — is it actually your contact?
Both matter, because they pull in opposite directions. Roughly one in five numbers on a purchased list doesn’t belong to the contact named on that row, and wrong numbers answer the phone more often than right ones — a stranger has no reason to screen an unknown caller, while your actual prospect screens everything. Optimizing for connect rate alone therefore selects for wrong numbers. OptimalDial Identity needs one extra input: the contact’s name, mapped at upload time via full_name_column or first_name_column + last_name_column. Without a name, identity is unavailable for that list.

OptimalDial_Status — the nine tiers

When identity is enabled, OptimalDial_Status holds the two axes crossed into one value. Take the Answer Intent value; if identity is Medium or better, insert “Right-Party”; if identity is Low, replace it with “Likely Wrong Number”. Each description states what the number is likely to do, then who it’s likely to belong to — in that order. Every tier except 7 - Non-Mobile is a prediction, which is why they all carry “Likely”; line type is a carrier attribute, so tier 7 states it outright. Ordering is answer-intent-major, and odd tiers are the identity-backed ones. The numeric prefix is there so the column sorts correctly as plain text in Sheets, Excel, and dialer imports. Treat this as a fixed vocabulary produced upstream — filter or sort on the numeric prefix rather than re-deriving the tier from the two axes, which will drift. Without identity, OptimalDial_Status holds the Answer Intent value instead (Likely Answer, Likely Call Screening, Likely Voicemail, Non-Mobile, Unknown), exactly as it always has.

The OptimalDial_Identity column

Opt in via include_identity on the download endpoint. Values are Very High, High, Medium, Low, and Unknown.
Unknown means we found no identity signal either way — it is not the same as “safe”. Measured against real call dispositions it sits between Medium and Low. Don’t treat it as a clean number, but don’t filter it out by default either: dropping it costs about as many good conversations as it avoids bad ones.

Validation tiers

Each upload runs at one of two tiers. Pass validation_tier on Create an upload; it defaults to standard. Credits are always whole numbers — max rounds the total up, so a 1,001-number list at max is charged 1502 credits. Check credits_charged on the response to see the exact amount deducted.

Multiple phone columns

Most CRM exports carry more than one number per contact — a mobile, a direct line, a switchboard. Map up to 10 of them in a single upload with phone_columns and every number is validated. Billing is per number. A contact with a number in two mapped columns costs 2 credits at standard, or ceil(2 × 1.5) = 3 at max — the same as two single-number contacts would. The same number listed in more than one column is de-duplicated and charged once, and so is a number that already appeared on an earlier contact. valid_number_count on the response is what you were charged for; valid_row_count is how many contacts those numbers belong to.
In JSON mode it’s a real array:

What comes back

The processed file keeps one row per contact — your file’s shape is unchanged. Each mapped column gets its own result columns, prefixed with that column’s header:
A blank status cell means that column held no number for that contact. With OptimalDial Identity enabled, each column also gets its own {column}_OptimalDial_Answer_Intent and {column}_OptimalDial_Identity. Filtering a processed download by status keeps a contact when any of their numbers matches, and sorting ranks each contact by their best number.
Map a single column — with phone_column, or a one-entry phone_columns — and nothing changes: you get the plain OptimalDial_Status column exactly as before. The prefixed shape only appears when you map more than one.

Create an upload

Submit a list of phone numbers and start processing. There are two content types — choose whichever matches what you have on hand.
  • multipart/form-data — upload a CSV file you already have on disk. Limit: 100 MB.
  • application/json — submit phone numbers (or contact objects) directly in the request body.
Both modes share these limits:
  • Minimum 100 valid phone numbers per upload. This counts numbers, not contacts — 50 contacts with two mapped phone columns each qualifies.
  • Maximum 250,000 phone numbers per request.
  • Numbers must resolve to a US or Canadian region (E.164-normalized server-side).
Calls are subject to rate limits — 60 / minute per API key, 600 / minute per organization.

CSV mode (multipart/form-data)

Supply either phone_column or phone_columns. Passing both is fine as long as they agree — if phone_column isn’t among phone_columns, the request returns 400 rather than us guessing which one you meant to be charged for. Supply either full_name_column, or both first_name_column and last_name_column; supplying only one half of the pair returns 400.

JSON mode (application/json)

Send either a flat phone_numbers array or a list of contacts objects with a phone_column field name. You must include exactly one of the two.
OptimalDial Identity needs a name to compare against, so it requires contacts — a flat phone_numbers array carries none. Passing a name column with phone_numbers returns 400.

Response

200 OK returns a fully populated Upload with status: "ready_for_processing".

Errors


List uploads

Cursor-paginated list of all uploads in the organization, newest first. Returns both web-uploaded and API-uploaded items; filter on source client-side if you only care about one.

Query parameters

Response

next_cursor is null when there are no more pages. Order is created_at DESC, then id DESC to break ties.

Retrieve an upload

Returns the latest snapshot of one upload. Useful as a fallback if you missed a webhook delivery.

Errors


Cancelling an upload

Uploads can’t be cancelled. Once you submit a list we take ownership of the work and the credits are committed, so there is no DELETE on this resource — DELETE /api/v1/uploads/{id} now returns 410 Gone. If you submitted something in error, email support@optimaldial.com with the upload id. We’ll refund the credits where the work hasn’t been done.

Download endpoints

These return short-lived signed download URLs — the actual file lives in object storage and is fetched directly from there.

Filtering the processed file

…/download/processed accepts optional query parameters. With none of them the response is exactly what it has always been — a URL to the full processed file. With any of them, we build the slice you asked for and point the URL at that instead. The response body is the same shape either way.
An unrecognized status returns 400 listing the valid values for that file.
…/download/processed-filtered is deprecated. It still works — older uploads serve their stored file and newer ones have the equivalent generated on demand — but it can only ever return one fixed slice. Prefer …/download/processed?statuses=…, which returns any slice you want.
All three responses share the same shape:
expires_at is a Unix timestamp; the signed URL is valid for 1 hour. Always re-fetch from this endpoint rather than caching the URL — once it expires, the URL returns 403.

Errors