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 select Create API key. Give it a description you will recognise later, such as the name of the tool that will use it. Copy the key when it appears, because it is shown once.
- Post a phone number to the endpoint below.
- Watch the call happen on the Outreach page. If nothing arrives, the key's entry on the Developers page still reads "Never used", which means the request never reached us.
Send "dryRun": true while you are wiring this up and the whole sequence is
planned and recorded without anybody's phone ringing.
curl -X POST https://www.asisto.one/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#
Every request is checked against the scope the endpoint needs.
| 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 issued from the dashboard carry all four, and so do keys created before scopes existed. There is no scope picker yet, so nothing you create needs configuring before it works, create a key and it can enrol contacts.
Because every key can do everything, create one key per tool rather than sharing one. Revoking a key takes effect on the next request, so a key you no longer recognise costs nothing to revoke.
Expiry and revocation#
A key either never expires or expires at the end of the day you pick, so a key set to expire on 4 March works for all of the 4th.
Revoking a key from the Developers page stops it immediately. It disappears from your list but is kept internally, because a key that started calls is part of the record of why those people were telephoned.
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 a daily 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, daily-contact-cap, reserved-sequence,
duplicate-request.
GET /api/v1/autodial/{id}#
Reports what has been tried and what is next. Requires autodial:read.
curl https://www.asisto.one/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, "perDay": 3 },
"messages": { "sent": 1, "remaining": 2, "perDay": 3 },
"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",
"heldForDailyLimit": false,
"deferredForQuietHours": false
}
}remaining is what is left of this enrollment's ladder; perDay is how much
the contact may receive in any twenty-four hours. The two are different
numbers, and next says which of them moved the step: heldForDailyLimit
means the contact has had their allowance for today, and deferredForQuietHours
means the time was outside the calling window. Neither is an error, the call
is still going to happen.
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 your key has a signing secret. There is no control for this in the dashboard yet, so unless somebody has set a secret on your key for you, nothing below applies and the bearer token over HTTPS is the whole contract.
When a key does have a signing secret, its requests must carry two extra headers, and requests without them are refused:
| 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://www.asisto.one/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 to 60 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.
There is one more, per contact rather than per business: a number that has
already had its day's allowance of calls is refused with
reason: "daily-contact-cap", also as 429. Like the others it is a pace
rather than a verdict, the same request tomorrow is accepted.
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://www.asisto.one/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.