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, cancel, 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.
FieldTypeNotes
idstring (UUID)Stable identifier.
user_idstring (UUID)The user who owns the API key that created this upload (or the web user who uploaded).
organization_idstring (UUID)The organization the API key is scoped to.
original_filenamestringThe filename you submitted, or one we synthesized for JSON-mode uploads (api_upload_<hex>.csv).
storage_pathstringInternal storage key. Use the download endpoints — never construct URLs from this directly.
file_size_bytesinteger | nullSize of the original file.
statusenumSee statuses below.
valid_row_countinteger | nullNumbers that passed validation and were charged.
invalid_row_countinteger | nullNumbers we couldn’t parse or that aren’t US/Canadian.
credits_requiredinteger | nullSame as valid_row_count at submission time.
credits_chargedinteger | nullWhat was actually deducted (will equal credits_required once the upload reaches ready_for_processing).
credits_refundedinteger | nullSet if you cancelled and credits were returned.
source"web" | "api"Which surface created this upload. API-created uploads always have "api".
api_key_idstring (UUID) | nullSet when source == "api".
skip_mobile_lookupboolean | nullMirrors the request flag.
processed_storage_pathstring | nullInternal path; populated when status is completed.
processed_filtered_storage_pathstring | nullInternal path for the “likely answer only” filtered file.
error_messagestring | nullPopulated when status == "failed".
created_atstring (ISO 8601)UTC timestamp.
updated_atstring (ISO 8601)UTC timestamp of the last status change.

Upload statuses

StatusMeaning
ready_for_processingThe upload was accepted and credits were charged. Returned by POST /api/v1/uploads.
processingWe’re working on it.
completedProcessing finished. Use the download endpoints.
failedProcessing failed; check error_message.
cancelledYou called DELETE /api/v1/uploads/{id}.
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.

Create an upload

POST /api/v1/uploads
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.
  • 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)

Form fieldTypeRequiredDescription
filefileyesThe CSV file. Must include a header row.
phone_columnstringyesName of the column containing phone numbers.
skip_mobile_lookupbooleannoSkip the mobile-vs-landline lookup step (defaults to false).
filename_overridestringnoUse this as original_filename instead of the file’s own name.
curl -X POST https://api.optimaldial.com/api/v1/uploads \
  -H "Authorization: Bearer $OPTIMALDIAL_API_KEY" \
  -F "file=@./leads.csv" \
  -F "phone_column=phone"

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.
FieldTypeRequiredDescription
phone_numbersstring[]one ofA flat array of phone numbers. The output CSV has one column called phone.
contactsobject[]one ofA list of arbitrary objects that include a phone field. Other keys are preserved through to the processed file.
phone_columnstringrequired with contactsThe key on each contact object that holds the phone number.
filenamestringnoUsed as original_filename. Defaults to api_upload_<hex>.csv.
skip_mobile_lookupbooleannoSkip the mobile-vs-landline lookup step.
curl -X POST https://api.optimaldial.com/api/v1/uploads \
  -H "Authorization: Bearer $OPTIMALDIAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "filename": "april-cohort.csv",
    "contacts": [
      {"phone": "+15551234567", "first_name": "Alex", "list": "april"},
      {"phone": "+15551234568", "first_name": "Bea",  "list": "april"}
      /* …at least 100 entries… */
    ],
    "phone_column": "phone"
  }'

Response

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

Errors

StatusWhen
400Missing required field, malformed JSON, both/neither of phone_numbers and contacts provided, phone_column not present in the data
401Missing, invalid, or revoked API key
402Insufficient credits to charge for the valid rows
403Organization has no active subscription
413CSV exceeds 100 MB, or phone_numbers/contacts exceeds 250,000
415Content-Type is neither multipart/form-data nor application/json
422Fewer than 100 valid phone numbers after validation. Body: {"detail": {"error":"min_contacts_required","min":100,"got":N,"message":...}}
429Rate limit hit (details)

List uploads

GET /api/v1/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

ParamTypeDefaultNotes
limitinteger (1–100)50Page size.
cursorstringPass back next_cursor from the previous response. Cursors are opaque; don’t construct them yourself.
curl "https://api.optimaldial.com/api/v1/uploads?limit=20" \
  -H "Authorization: Bearer $OPTIMALDIAL_API_KEY"

Response

{
  "data": [ /* Upload, Upload, … */ ],
  "next_cursor": "2026-04-24T12:34:56.789+00:00|f3c9b8a7-1234-4abc-9def-0123456789ab"
}
next_cursor is null when there are no more pages. Order is created_at DESC, then id DESC to break ties.

Retrieve an upload

GET /api/v1/uploads/{upload_id}
Returns the latest snapshot of one upload. Useful as a fallback if you missed a webhook delivery.
curl "https://api.optimaldial.com/api/v1/uploads/$UPLOAD_ID" \
  -H "Authorization: Bearer $OPTIMALDIAL_API_KEY"

Errors

StatusWhen
401Auth failure
404Upload doesn’t exist, or belongs to a different organization

Cancel an upload

DELETE /api/v1/uploads/{upload_id}
Cancels an upload only if it is still in ready_for_processing. Any credits that were charged at submission are refunded to the organization’s balance. Once we’ve started processing, cancellation is no longer possible — at that point the work has already been done. The endpoint will return 200 with credits_refunded: 0 (current behaviour) but the upload will stay in its current status; check status before assuming the cancel succeeded.
curl -X DELETE "https://api.optimaldial.com/api/v1/uploads/$UPLOAD_ID" \
  -H "Authorization: Bearer $OPTIMALDIAL_API_KEY"

Response

{ "status": "cancelled", "credits_refunded": 100 }
credits_refunded is 0 if the upload had no credits to refund (e.g. cancellation came in after processing started, or the upload was never charged).

Download endpoints

GET /api/v1/uploads/{upload_id}/download/original
GET /api/v1/uploads/{upload_id}/download/processed
GET /api/v1/uploads/{upload_id}/download/processed-filtered
These return short-lived signed download URLs — the actual file lives in object storage and is fetched directly from there.
PathWhat it returnsWhen it’s ready
…/download/originalThe CSV exactly as you submitted it (or the CSV we built from your JSON contacts).Immediately after POST /api/v1/uploads returns.
…/download/processedThe full processed file with answer-likelihood classifications added per row.After status becomes completed.
…/download/processed-filteredA filtered subset containing only the “likely answer” rows.After status becomes completed.
All three responses share the same shape:
{
  "download_url": "https://storage.example.com/...?signed=...",
  "expires_at": 1714003600.123
}
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.
DL=$(curl -s "https://api.optimaldial.com/api/v1/uploads/$UPLOAD_ID/download/processed" \
       -H "Authorization: Bearer $OPTIMALDIAL_API_KEY")
URL=$(echo "$DL" | jq -r .download_url)
curl -L -o processed.csv "$URL"

Errors

StatusWhen
401Auth failure
404Upload doesn’t exist, or the requested file isn’t ready yet (e.g. you asked for processed while status is still processing)