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 anUpload. Most fields are populated incrementally as the upload moves through its lifecycle.
| Field | Type | Notes |
|---|---|---|
id | string (UUID) | Stable identifier. |
user_id | string (UUID) | The user who owns the API key that created this upload (or the web user who uploaded). |
organization_id | string (UUID) | The organization the API key is scoped to. |
original_filename | string | The filename you submitted, or one we synthesized for JSON-mode uploads (api_upload_<hex>.csv). |
storage_path | string | Internal storage key. Use the download endpoints — never construct URLs from this directly. |
file_size_bytes | integer | null | Size of the original file. |
status | enum | See statuses below. |
valid_row_count | integer | null | Numbers that passed validation and were charged. |
invalid_row_count | integer | null | Numbers we couldn’t parse or that aren’t US/Canadian. |
credits_required | integer | null | Same as valid_row_count at submission time. |
credits_charged | integer | null | What was actually deducted (will equal credits_required once the upload reaches ready_for_processing). |
credits_refunded | integer | null | Set if you cancelled and credits were returned. |
source | "web" | "api" | Which surface created this upload. API-created uploads always have "api". |
api_key_id | string (UUID) | null | Set when source == "api". |
skip_mobile_lookup | boolean | null | Mirrors the request flag. |
processed_storage_path | string | null | Internal path; populated when status is completed. |
processed_filtered_storage_path | string | null | Internal path for the “likely answer only” filtered file. |
error_message | string | null | Populated when status == "failed". |
created_at | string (ISO 8601) | UTC timestamp. |
updated_at | string (ISO 8601) | UTC timestamp of the last status change. |
Upload statuses
| Status | Meaning |
|---|---|
ready_for_processing | The upload was accepted and credits were charged. Returned by POST /api/v1/uploads. |
processing | We’re working on it. |
completed | Processing finished. Use the download endpoints. |
failed | Processing failed; check error_message. |
cancelled | You called DELETE /api/v1/uploads/{id}. |
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
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.
- 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).
CSV mode (multipart/form-data)
| Form field | Type | Required | Description |
|---|---|---|---|
file | file | yes | The CSV file. Must include a header row. |
phone_column | string | yes | Name of the column containing phone numbers. |
skip_mobile_lookup | boolean | no | Skip the mobile-vs-landline lookup step (defaults to false). |
filename_override | string | no | Use this as original_filename instead of the file’s own name. |
- cURL
- Node.js
- Python
JSON mode (application/json)
Send either a flatphone_numbers array or a list of contacts objects with a phone_column field name. You must include exactly one of the two.
| Field | Type | Required | Description |
|---|---|---|---|
phone_numbers | string[] | one of | A flat array of phone numbers. The output CSV has one column called phone. |
contacts | object[] | one of | A list of arbitrary objects that include a phone field. Other keys are preserved through to the processed file. |
phone_column | string | required with contacts | The key on each contact object that holds the phone number. |
filename | string | no | Used as original_filename. Defaults to api_upload_<hex>.csv. |
skip_mobile_lookup | boolean | no | Skip the mobile-vs-landline lookup step. |
- cURL
- Node.js
- Python
Response
200 OK returns a fully populated Upload with status: "ready_for_processing".
Errors
| Status | When |
|---|---|
400 | Missing required field, malformed JSON, both/neither of phone_numbers and contacts provided, phone_column not present in the data |
401 | Missing, invalid, or revoked API key |
402 | Insufficient credits to charge for the valid rows |
403 | Organization has no active subscription |
413 | CSV exceeds 100 MB, or phone_numbers/contacts exceeds 250,000 |
415 | Content-Type is neither multipart/form-data nor application/json |
422 | Fewer than 100 valid phone numbers after validation. Body: {"detail": {"error":"min_contacts_required","min":100,"got":N,"message":...}} |
429 | Rate limit hit (details) |
List uploads
source client-side if you only care about one.
Query parameters
| Param | Type | Default | Notes |
|---|---|---|---|
limit | integer (1–100) | 50 | Page size. |
cursor | string | — | Pass back next_cursor from the previous response. Cursors are opaque; don’t construct them yourself. |
- cURL
- Node.js
- Python
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
- cURL
- Node.js
- Python
Errors
| Status | When |
|---|---|
401 | Auth failure |
404 | Upload doesn’t exist, or belongs to a different organization |
Cancel an upload
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
- Node.js
- Python
Response
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
| Path | What it returns | When it’s ready |
|---|---|---|
…/download/original | The CSV exactly as you submitted it (or the CSV we built from your JSON contacts). | Immediately after POST /api/v1/uploads returns. |
…/download/processed | The full processed file with answer-likelihood classifications added per row. | After status becomes completed. |
…/download/processed-filtered | A filtered subset containing only the “likely answer” rows. | After status becomes completed. |
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.
- cURL
- Node.js
- Python
Errors
| Status | When |
|---|---|
401 | Auth failure |
404 | Upload doesn’t exist, or the requested file isn’t ready yet (e.g. you asked for processed while status is still processing) |