Docs
Autodial API & Webhooks

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#

  1. Go to Settings, Organization, Developers and create an API key with the autodial:write scope. Copy it, because it is shown once.
  2. Post a phone number to the endpoint below.
  3. 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.

ScopeAllows
autodial:writeEnrolling a contact, which starts calls
autodial:readReading the status of an enrollment
contacts:readReading contact records
contacts:writeCreating 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#

FieldTypeRequiredNotes
phonestringyesAny readable format. Without a + prefix it is read as a number in your own country
namestringnoUsed to greet them and saved to the contact
emailstringnoSaved to the contact
consentobjectusuallyRequired unless the consent rule is turned off for the business
consent.givenbooleanyes, within consentMust be true to satisfy the rule
consent.sourcestringnoWhere consent came from, such as web-form
consent.evidencestringnoA URL, form id, or anything you could produce if challenged
consent.externalRefstringnoThe record id in your own system
consent.capturedAtISO 8601noWhen consent was given. Defaults to now
businessIdUUIDonly with several businessesWhich business is calling
campaignstringnoWhich sequence to run, if you have more than one
scheduledAtISO 8601noEarliest time to start. Still held to the calling window
idempotencyKeystringnoSend the same key twice and the second request changes nothing
metadataobjectnoAnything you want the AI to know about this person
dryRunbooleannoPlan and record the whole sequence without placing a real call

Two metadata keys are read rather than simply stored:

  • timeZone or timezone: an IANA timezone such as America/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#

CodeMeaning
202Accepted. The first call is scheduled
400The number is unreadable, a field is invalid, or a businessId is needed
401The API key is missing, expired, revoked, or the signature failed
403The key does not carry the autodial:write scope
404No such business, or the business is not yours
409Already enrolled, called recently, or on your do-not-call list
422No consent record, and this business requires one
429Rate 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#

OutcomeMeaning
connectedSpoke with the contact
screened_connectedReached them after answering call screening
bookedAn appointment was booked on the call
no_answerNobody picked up, or they hung up within seconds
busyThe line was busy
voicemail_leftAn answering machine received the message
machine_no_messageAn answering machine, message not left
wrong_numberReached somebody else. Number suppressed
dead_numberNot in service. Number suppressed
opted_outAsked not to be called. Number suppressed
sms_sentA follow-up text went out
sms_failedA text could not be sent
skippedThe step was not taken, with the reason in detail
failedThe 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:

HeaderValue
X-Asisto-TimestampCurrent Unix time in seconds
X-Asisto-SignatureHMAC-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_here and Content-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.