Orders
An order asks an agent to call one lead, up to three times, and report the result. You create it with POST /v1/orders and read it with GET /v1/orders/{order_id}. GET /v1/orders lists your finished orders for polling.
Create an order
POST https://api.avoonta.com/v1/orders
Authorization: Bearer av_live_…
Content-Type: application/json
Idempotency-Key: crm_contact_8821 (optional)
Example request body
{
"agent_id": "agt_3f9c2a7b1d0e4c58a6b9e2f1d4c7a0b3",
"lead": {
"phone": "+14155550199",
"first_name": "Daniel",
"last_name": "Cohen",
"email": "daniel@acme.example",
"company": "Acme Solar",
"custom_context": "Requested a quote for a 10kW home solar system via a Facebook ad."
},
"cadence": {
"max_attempts": 3,
"retry_interval_minutes": 120,
"send_sms_on_voicemail": true
},
"consent": {
"opt_in_confirmed": true,
"consented_at": "2026-10-14T09:00:00Z",
"source": "https://acme.example/quote-form"
},
"webhook_url": "https://crm.example.com/webhooks/avoonta",
"external_reference_id": "crm_contact_8821"
}
Fields
| Field | Type | Rules |
|---|---|---|
agent_id required | string | The agent's public ID, agt_ plus 32 hex characters (a plain UUID is also accepted). The agent must be in the key's workspace, switched on, and published. |
lead required | object | Who to call. |
lead.phone required | string | E.164 is best (+14155550199). Spaces, dashes, dots and brackets are removed; a 10-digit number, or 11 digits starting with 1, is read as a US number. The result must be +1 and 10 digits. |
lead.first_name | string | The agent uses it to greet the lead and in the fallback text. Trimmed, cut to 200 characters. |
lead.last_name | string | Trimmed, cut to 200 characters. |
lead.email | string | Stored as given (not validated). Cut to 200 characters. |
lead.company | string | Cut to 200 characters. |
lead.custom_context | string | What the lead asked for, in plain words. Passed to the agent. Cut to 2,000 characters. |
cadence | object | Optional. How hard to try. Each field you leave out takes the agent's follow-up plan (set in the app on the agent's Follow-up tab); a field you send overrides it for this order only. |
cadence.max_attempts | integer | 1, 2 or 3. Default: the agent's follow-up plan (3 until you change it). |
cadence.retry_interval_minutes | integer | 5 to 10080 (7 days). Default: the agent's follow-up plan (120 until you change it). The next attempt is moved later if it would fall outside the lead's calling hours or your business calling hours. |
cadence.send_sms_on_voicemail | boolean | Default: the agent's follow-up plan (true until you change it). Send one text with your booking link when nobody (or a voicemail machine) answers. At most one text per order. See Fallback text. |
consent required | object | Proof that the lead agreed to be called. See Consent. |
consent.opt_in_confirmed required | boolean | Must be the JSON value true (not the string "true"). |
consent.consented_at required | string | ISO 8601 with a date, a time and a zone: 2026-10-14T09:00:00Z or 2026-10-14T05:00:00-04:00. No more than 5 minutes in the future, and no more than 1,461 days (4 years) old. |
consent.source | string | Where the lead agreed: a form URL or a short label. Stored as given, cut to 2,000 characters. Strongly recommended. |
webhook_url | string or null | Where to send this order's result. Must start with https://, at most 2,000 characters. If you leave it out, the workspace's default webhook URL is used (if one is set). |
external_reference_id | string or null | Your own ID for this lead or contact, at most 200 characters. Sent back in every webhook. Not unique and not used to detect duplicates: use Idempotency-Key for that. |
Any other field in the body or in lead is ignored. Only opt_in_confirmed, consented_at and source are kept from consent.
Consent (required)
Every order must carry the lead's consent. This is checked first, before your API key, your wallet or anything else. If consent is missing, opt_in_confirmed is not true, or consented_at is missing, has no time zone, is not a real date, or is more than 5 minutes in the future, the order is refused:
HTTP/1.1 400 Bad Request
{
"success": false,
"error": "consent_required",
"message": "consent.opt_in_confirmed must be true and consent.consented_at must be an ISO 8601 timestamp."
}
A consented_at older than 1,461 days passes the first check but is refused when the order is saved, with 400 invalid_request ("The order was refused by validation. Check consent, phone and cadence.").
The consent object is stored with the order for good and cannot be changed. It is the audit record for every call and text on that order. Read Compliance for what consent you need to collect.
What is checked, in order
- The body is a JSON object, else
400 invalid_json. consent, else400 consent_required.- The API key, else
401 unauthorized. - The fields above, else
400 invalid_requestwith a message naming the field. - The agent:
404 agent_not_found,409 agent_inactive. - If you sent an
Idempotency-Keyalready used in this workspace, the original order is returned here. - The agent is published, else
409 agent_not_published. - The phone number:
400 invalid_phone;422 unsupported_destinationwhen it is not a number in the US mainland, Alaska or Hawaii (Canada, the Caribbean, Puerto Rico and the other US territories, and the other countries that share+1are not called in v1);422 high_cost_destinationfor a high-cost US area. - The wallet has at least $1.00 and the plan is active, else
402 insufficient_balance. Skipped for test keys. - The number is not on your workspace's do-not-contact list, else
409 do_not_contact. If the list cannot be read we refuse with503 suppression_unavailable; we never call when we cannot check. - The lead's calling hours. Outside 8am to 8pm in the lead's time zone, the order is accepted and waits for the window to open.
Every code is explained on the Errors page.
Response: 202 Accepted
HTTP/1.1 202 Accepted
{
"success": true,
"order_id": "ord_9b1e0c4d2a7f4e8b9c3d5a6f7e8d9c0b",
"status": "QUEUED",
"estimated_dial_seconds": 15,
"wallet_balance_cents": 4850
}
| Field | Meaning |
|---|---|
order_id | The order's ID: ord_ plus 32 hex characters. Keep it to match webhooks. |
status | Always QUEUED for a new order. A replayed order shows its current status. |
estimated_dial_seconds | Seconds until the first dial is due, never less than 15. Large when the lead's calling hours have not started yet. The dial can still wait longer if your live-call limit is reached (see Limits). |
wallet_balance_cents | Your wallet balance in cents when the order was taken. |
test | Present and true only for an order made with an av_test_ key: a simulated call, see Test keys. |
Idempotency
Send an Idempotency-Key header to make retries safe. If your request times out and you send it again with the same key, you get the first order back instead of a second call.
- The key is 1 to 200 visible ASCII characters (no spaces). Anything else gets
400 invalid_request. - Keys are scoped to your workspace and do not expire.
- A repeat returns
202with the originalorder_id, its currentstatus, and the response headerIdempotent-Replayed: true. - The body of the repeat is not compared with the first one. Same key, same order, even if the body changed.
- The repeat is still validated (consent, key, fields, agent) before the original order is looked up.
- A key used by a test order cannot be reused with a live key, nor the reverse:
409 idempotency_key_conflict. - Two requests with the same key at the same moment still make only one order.
Read an order
GET https://api.avoonta.com/v1/orders/ord_9b1e0c4d2a7f4e8b9c3d5a6f7e8d9c0b
Authorization: Bearer av_live_…
HTTP/1.1 200 OK
{
"success": true,
"order_id": "ord_9b1e0c4d2a7f4e8b9c3d5a6f7e8d9c0b",
"external_reference_id": "crm_contact_8821",
"direction": "OUTBOUND_ORDER",
"status": "RETRY_SCHEDULED",
"attempt_number": 1,
"duration_seconds": 0,
"billed_minutes": 0,
"charged_cents": 0,
"next_retry_at": "2026-10-14T18:05:00+00:00",
"qualification": null,
"appointment": null,
"recording_url": null,
"transcript": [],
"reason": "no_answer",
"test": false,
"created_at": "2026-10-14T16:04:31.112+00:00",
"updated_at": "2026-10-14T16:05:02.540+00:00",
"finished_at": null
}
The fields are the same as in the webhook payload (including reason and test), plus direction (OUTBOUND_ORDER, or INBOUND_CALL for a call someone made to your number), next_retry_at, created_at, updated_at and finished_at. updated_at changes on every write, including after the call (recording, transcript, webhook delivery). finished_at is the time the order reached its final status and does not change after that; it is null while the order is still in progress or waiting for a retry. Unlike the webhook, appointment is null when nothing was booked.
An order of another workspace, or an ID that does not exist, returns 404 not_found.
List orders
Lists your orders, most recently changed first (or most recently finished first with sort=finished_at). By default it returns finished orders only, so you can poll it for results (this is what the Zapier "Call finished" and Make "Watch Finished Calls" triggers do).
GET https://api.avoonta.com/v1/orders?status=BOOKED,QUALIFIED&sort=finished_at&since=2026-10-14T00:00:00Z&limit=20
Authorization: Bearer av_live_…
| Query | Rules |
|---|---|
status | A comma-separated list of statuses, in any case. Default: the finished statuses BOOKED, QUALIFIED, DISQUALIFIED, FAILED and VOICEMAIL_SMS_SENT. |
sort | updated_at (default) or finished_at. With finished_at, orders are listed newest finish first, only orders that have finished are listed, and since applies to finished_at. Use it to poll for results: a finished order keeps its place even when it is written to later. |
since | Only orders whose updated_at (or finished_at, with sort=finished_at) is at or after this time. ISO 8601 with a zone (2026-10-14T09:00:00Z) or a bare date (2026-10-14, midnight UTC). |
limit | 1 to 100. Default 50. |
agent_id | Only this agent's orders (agt_…). |
HTTP/1.1 200 OK
{
"success": true,
"orders": [
{
"id": "ord_9b1e0c4d2a7f4e8b9c3d5a6f7e8d9c0b",
"order_id": "ord_9b1e0c4d2a7f4e8b9c3d5a6f7e8d9c0b",
"external_reference_id": "crm_contact_8821",
"direction": "OUTBOUND_ORDER",
"status": "BOOKED",
"attempt_number": 1,
"duration_seconds": 134,
"billed_minutes": 3,
"charged_cents": 75,
"next_retry_at": null,
"qualification": { "qualified": true, "score": 92, "extracted_answers": { "homeowner": "Yes" }, "summary": "Booked a site visit." },
"appointment": { "booked": true, "start_time": "2026-10-22T14:00:00Z", "calendar_event_id": "cal_5519" },
"recording_url": "https://api.avoonta.com/v1/recordings/ord_9b1e0c4d2a7f4e8b9c3d5a6f7e8d9c0b.mp3",
"transcript": [{ "role": "agent", "text": "Hi Daniel…" }],
"reason": null,
"test": false,
"created_at": "2026-10-14T16:04:31.112+00:00",
"updated_at": "2026-10-14T16:07:12.004+00:00",
"finished_at": "2026-10-14T16:06:58.310+00:00"
}
],
"has_more": false
}
- Each item is the same object as
GET /v1/orders/{order_id}, plusid, a copy oforder_idthat integrations use to skip orders they have already seen. has_moreistruewhen more orders match thanlimit. To read further back, narrow the list withsinceorstatus.- A live key lists live orders only, and a test key test orders only.
- A wrong query value returns
400 invalid_requestwith a message naming it. Reads share a limit of 120 requests per minute per workspace (429 rate_limitedwithRetry-After).
Coming soon Searching orders by external_reference_id, and cancelling a queued order through the API.
Order lifecycle
| Status | Meaning | Final? |
|---|---|---|
QUEUED | Accepted, waiting for its first dial. It also waits here for the lead's calling hours, your live-call limit, or enough wallet balance to start a call. A wait for money is never silent: reason becomes waiting_for_balance (or plan_inactive / plan_past_due), you get one order.updated webhook with the status unchanged, and the workspace gets one alert. The order dials by itself once the wallet covers the start of a call (the larger of $1.00 and 5 minutes at your plan rate). | No |
DIALING | The phone is ringing. We listen for a person or a voicemail machine before the agent speaks. | No |
IN_PROGRESS | A person answered and is talking to the agent. | No |
BOOKED | The call ended and the agent booked an appointment. | Yes |
QUALIFIED | The call ended without a booking and the lead was not marked unqualified. Check qualification.qualified: true, or null when the agent did not score the lead. | Yes |
DISQUALIFIED | The call ended and the agent marked the lead as not qualified. | Yes |
VOICEMAIL_SMS_SENT | Nobody (or a machine) answered and the fallback text was sent. Final only when no attempts are left; otherwise the order moves straight on to RETRY_SCHEDULED. | When no attempts are left |
RETRY_SCHEDULED | Nobody answered; the next attempt is due at next_retry_at. | No |
FAILED | The order ended without a conversation: all attempts used with no answer and no text sent, or the call could not be placed (for example the number asked not to be contacted after the order was made, the agent was switched off, or the workspace has no active phone number). | Yes |
QUEUED ─▶ DIALING ─┬─▶ IN_PROGRESS ─▶ BOOKED | QUALIFIED | DISQUALIFIED | FAILED
├─▶ VOICEMAIL_SMS_SENT ─▶ RETRY_SCHEDULED ─▶ DIALING …
├─▶ RETRY_SCHEDULED ─▶ DIALING …
└─▶ FAILED
- One attempt is one dial.
attempt_numbercounts dials made so far. - A voicemail machine is detected before the agent speaks, and we hang up. You are not charged for it.
- On a surcharged number, after a machine answers we try at most once more.
- If a dial cannot start because of calling hours or a short outage on our side, the order goes back to the queue and the attempt is not counted.
- A call is cut off after 30 minutes.
reasonsays why an order is waiting or why it failed, for examplewaiting_for_balance,plan_inactive,no_answer,machine,do_not_contact,unsupported_destination,agent_inactive,no_caller_id. It isnullwhen there is nothing to report, and is cleared when the next attempt starts.- An order whose number turns out not to be a US number when it is about to dial (for example one queued before a rule change) ends as
FAILEDwithreason: "unsupported_destination"; it is never dialled.
Fallback text
When send_sms_on_voicemail is true and nobody answers, we send one text per order:
Hi Daniel, this is Alex from Acme Solar. We tried to call you about your request. Pick a time that suits you: https://… Reply STOP to opt out.
The text is sent only when all of these hold: the lead is in their 8am to 8pm window, the number is not on the do-not-contact list, your workspace has a booking link (from a connected calendar), and your sending number is registered for business texting. If any of these fails, no text is sent and the order simply moves to its next attempt. Texts are charged per segment.