Autodial API & Webhooks
Send a lead from any system and Asisto calls them. Full reference for the autodial endpoint, authentication, signing, and inbound HubSpot webhooks.
Send Asisto a phone number and it calls that person, following the rules your business has configured. Anything that can make a web request can do it: your website, a form builder, Zapier, Make, n8n, or your own backend.
Quick start#
- Go to Settings, Organization, Developers and create an API key with the
autodial:writescope. Copy it, because it is shown once. - Post a phone number to the endpoint below.
- Watch the call happen on the Outreach page.
curl -X POST https://your-domain.com/api/v1/autodial \
-H "Authorization: Bearer api_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"phone": "+14155550111",
"name": "Dana Reed",
"email": "dana@example.com",
"consent": {
"given": true,
"source": "web-form",
"evidence": "https://example.com/contact-form"
},
"idempotencyKey": "form-submission-8842"
}'Authentication#
Send your API key as a bearer token:
Authorization: Bearer api_your_key_here
Some low-code tools cannot set an Authorization header. Those may send the key
as X-Api-Key instead. Both are equivalent.
Scopes#
Keys carry scopes, so a key that only reports on progress cannot start phone calls.
| Scope | Allows |
|---|---|
autodial:write | Enrolling a contact, which starts calls |
autodial:read | Reading the status of an enrollment |
contacts:read | Reading contact records |
contacts:write | Creating and updating contact records |
Keys created before scopes existed carry all four, so nothing already running was affected.
A missing key, an expired key and a revoked key all return the same 401 with
the same message. This is deliberate: distinguishing them would let anybody
probe which keys are real.
POST /api/v1/autodial#
Enrols a contact and schedules the first call. Returns immediately with 202;
the call itself happens in the background.
Request body#
| Field | Type | Required | Notes |
|---|---|---|---|
phone | string | yes | Any readable format. Without a + prefix it is read as a number in your own country |
name | string | no | Used to greet them and saved to the contact |
email | string | no | Saved to the contact |
consent | object | usually | Required unless the consent rule is turned off for the business |
consent.given | boolean | yes, within consent | Must be true to satisfy the rule |
consent.source | string | no | Where consent came from, such as web-form |
consent.evidence | string | no | A URL, form id, or anything you could produce if challenged |
consent.externalRef | string | no | The record id in your own system |
consent.capturedAt | ISO 8601 | no | When consent was given. Defaults to now |
businessId | UUID | only with several businesses | Which business is calling |
campaign | string | no | Which sequence to run, if you have more than one |
scheduledAt | ISO 8601 | no | Earliest time to start. Still held to the calling window |
idempotencyKey | string | no | Send the same key twice and the second request changes nothing |
metadata | object | no | Anything you want the AI to know about this person |
dryRun | boolean | no | Plan and record the whole sequence without placing a real call |
Two metadata keys are read rather than simply stored:
timeZoneortimezone: an IANA timezone such asAmerica/Denver. Given one, the calling window uses it instead of inferring a zone from the area code, and the hour of safety margin at each end is dropped.
Successful response#
{
"id": "9f2c8a1e-4d3b-4c7a-9f21-6b0d5e8c7a44",
"status": "accepted",
"deduplicated": false,
"firstAttemptAt": "2026-09-02T18:00:00.000Z",
"statusUrl": "/api/v1/autodial/9f2c8a1e-4d3b-4c7a-9f21-6b0d5e8c7a44"
}firstAttemptAt may be later than you asked for. If the contact is in a
timezone where it is 3am, or your business is closed, the call waits for the
next legal moment. deduplicated is true when an idempotency key matched an
existing enrollment, in which case nothing new was created.
Status codes#
| Code | Meaning |
|---|---|
202 | Accepted. The first call is scheduled |
400 | The number is unreadable, a field is invalid, or a businessId is needed |
401 | The API key is missing, expired, revoked, or the signature failed |
403 | The key does not carry the autodial:write scope |
404 | No such business, or the business is not yours |
409 | Already enrolled, called recently, or on your do-not-call list |
422 | No consent record, and this business requires one |
429 | Rate limited, or the daily new-contact limit is reached |
Every refusal carries a reason you can branch on, so your own system can tell
a business rule from a fault:
{
"error": "That contact is already being called.",
"reason": "already-enrolled",
"enrollmentId": "9f2c8a1e-4d3b-4c7a-9f21-6b0d5e8c7a44"
}Possible reasons: invalid-phone, unreachable-destination,
business-not-found, no-sequence, sequence-disabled, sequence-paused,
no-caller-id, consent-missing, suppressed, already-enrolled,
cooldown, daily-cap, duplicate-request.
GET /api/v1/autodial/{id}#
Reports what has been tried and what is next. Requires autodial:read.
curl https://your-domain.com/api/v1/autodial/9f2c8a1e-4d3b-4c7a-9f21-6b0d5e8c7a44 \
-H "Authorization: Bearer api_your_key_here"{
"id": "9f2c8a1e-4d3b-4c7a-9f21-6b0d5e8c7a44",
"status": "active",
"phone": "+14155550111",
"name": "Dana Reed",
"source": "webhook",
"createdAt": "2026-09-02T18:00:00.000Z",
"endedAt": null,
"endedReason": null,
"dryRun": false,
"calls": { "attempted": 1, "remaining": 2 },
"messages": { "sent": 1, "remaining": 2 },
"attempts": [
{
"kind": "call",
"attempt": 1,
"outcome": "no_answer",
"wasScreened": false,
"talkSeconds": 0,
"at": "2026-09-02T18:00:04.000Z",
"detail": null
},
{
"kind": "sms",
"attempt": 1,
"outcome": "sms_sent",
"wasScreened": false,
"talkSeconds": null,
"at": "2026-09-02T18:00:40.000Z",
"detail": null
}
],
"next": { "kind": "call", "scheduledAt": "2026-09-02T18:20:00.000Z" }
}An enrollment id from another organization returns 404 rather than 403, so
the endpoint cannot be used to confirm that an id exists.
Outcomes#
| Outcome | Meaning |
|---|---|
connected | Spoke with the contact |
screened_connected | Reached them after answering call screening |
booked | An appointment was booked on the call |
no_answer | Nobody picked up, or they hung up within seconds |
busy | The line was busy |
voicemail_left | An answering machine received the message |
machine_no_message | An answering machine, message not left |
wrong_number | Reached somebody else. Number suppressed |
dead_number | Not in service. Number suppressed |
opted_out | Asked not to be called. Number suppressed |
sms_sent | A follow-up text went out |
sms_failed | A text could not be sent |
skipped | The step was not taken, with the reason in detail |
failed | The attempt failed for a technical reason |
Signing requests#
Optional, and off unless you add a signing secret to your key. When you do, requests must carry two extra headers:
| Header | Value |
|---|---|
X-Asisto-Timestamp | Current Unix time in seconds |
X-Asisto-Signature | HMAC-SHA256(timestamp + "." + rawBody), hex encoded |
The timestamp is inside the signed material rather than beside it, so a captured request cannot have its window extended by rewriting it. Requests older than five minutes are refused.
const crypto = require('crypto');
const body = JSON.stringify({ phone: '+14155550111' });
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = crypto
.createHmac('sha256', process.env.ASISTO_SIGNING_SECRET)
.update(`${timestamp}.${body}`)
.digest('hex');
await fetch('https://your-domain.com/api/v1/autodial', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ASISTO_API_KEY}`,
'Content-Type': 'application/json',
'X-Asisto-Timestamp': timestamp,
'X-Asisto-Signature': signature
},
body
});Sign the exact bytes you send. Re-serialising parsed JSON changes the whitespace and breaks an otherwise correct signature.
Rate limits#
Accepted enrollments are limited per key per minute. Over the limit, the
endpoint returns 429 with a Retry-After header in seconds. Refused requests
do not count against it, because a refusal costs nothing and an accepted one
telephones somebody.
Separately, each business has a daily limit on new contacts. That limit also
returns 429, with reason: "daily-cap", and exists so that a bulk import into
your CRM does not turn into a calling list.
Connecting without code#
Any tool that posts JSON works. In Zapier, Make or n8n, add a Webhooks or HTTP Request step with:
- Method:
POST - URL:
https://your-domain.com/api/v1/autodial - Headers:
Authorization: Bearer api_your_key_hereandContent-Type: application/json - Body: JSON containing at least
phone
Map your form's phone field to phone, and set idempotencyKey to the
submission id so a replayed run does not call anybody twice.
HubSpot#
HubSpot does not use this endpoint. Connect it from Settings, Organization, Integrations and Asisto subscribes to contact creation directly, which is faster than any polling loop and needs no configuration on your side.
- New contacts are picked up within seconds, with a sixty-second backfill poll catching anything a delivery missed.
- Only contacts matching your enrollment filter are called. By default that is a lead-stage contact with a phone number created in the last day, which is deliberately narrow so connecting your CRM never turns your whole customer list into a calling list.
- Mobile numbers are preferred over landlines.
- Every attempt is written back to the contact as a logged call, along with properties for the last outcome, the attempt count and the last call time.
- Disconnecting stops new contacts being picked up. Sequences already running finish, because those people were already told they would be called.
Outbound webhooks#
Separately from the autodial endpoint, Asisto can call your URL when something happens. Configure these under Settings, Organization, Developers.
Every delivery is a POST with a JSON body:
{
"trigger": "contactCreated",
"createdAt": "2026-09-02T18:00:00.000Z",
"payload": {}
}If you set a secret on the webhook, the body is signed with HMAC-SHA256 and sent
as X-Webhook-Signature-256. Verify it by computing the same HMAC over the raw
request body and comparing in constant time. Without a secret the header reads
no-secret-provided, which you should treat as unsigned.
Deliveries are retried up to three times on failure. Respond 2xx quickly and
do the work afterwards, rather than holding the connection open.
Testing safely#
Send "dryRun": true and the entire sequence is planned, scheduled and recorded
without a single real call or text. It appears on the Outreach page marked as a
dry run, which makes it a genuine rehearsal rather than a mock.
Ready to get started?
Try our AI receptionist free and see the difference it makes for your business.