Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.thinnest.ai/llms.txt

Use this file to discover all available pages before exploring further.

Retry & Escalation

When an outbound voice call doesn’t connect — the contact didn’t answer, the line was busy, or it went to voicemail — your campaign can automatically:
  1. Retry the contact later, on a custom schedule (uniform or per-step).
  2. Escalate to a human via email or webhook once retries are exhausted (or if the contact permanently failed).
Both are configured per-campaign in Advanced Settings of the new-campaign modal.

Retry on No Answer

Failed outcomes that trigger a retry: no_answer, busy, voicemail. Instead of a single interval that applies to every retry, you can specify a list of delays — one entry per retry, in order. The first retry runs after the initial attempt fails.
RetryDelay
#11 hour (60 minutes)
#25 hours (300 minutes)
#324 hours (1440 minutes)
Builder UI in the campaign modal:
Retry schedule — one row per retry. First retry runs after the initial attempt fails.

  Retry #1   after [   60 ] minutes  ✕
  Retry #2   after [  300 ] minutes  ✕
  Retry #3   after [ 1440 ] minutes  ✕

  + Add another retry

  Example: 60, 300, 1440 = retry after 1 h, then 5 h, then 24 h.
The number of rows is the number of retries the platform will make (the original attempt is not counted). Maximum 10 retries per contact.

Uniform mode (legacy)

Older campaigns saved a single intervalMinutes value applied to every retry, plus a maxAttempts count. Those campaigns continue to work — the backend reads the legacy shape and treats it as the equivalent per-step list.

Persisted shape

In the campaign’s config JSONB column:
{
  "retry": {
    "enabled": true,
    "intervalsMinutes": [60, 300, 1440]
  }
}
Both shapes are accepted on save:
// New per-step (preferred)
{ "enabled": true, "intervalsMinutes": [60, 300, 1440] }

// Uniform (legacy, still works)
{ "enabled": true, "maxAttempts": 3, "intervalMinutes": 60 }
When both are present, per-step wins and the length of intervalsMinutes dictates the attempt count.

Behaviour notes

  • Retries are scheduled via the same arq deferred-job pipeline used elsewhere in the platform (proven in production).
  • If the contact’s wallet has no funds at retry time, the dispatch is skipped and logged — no infinite loop.
  • DND / TRAI compliance checks run on every retry, the same as the original attempt.
  • A retry that succeeds (caller picks up) ends the chain immediately for that contact.

Escalation

Escalation fires once per contact when the contact has either:
  • Exhausted every configured retry without picking up, or
  • Hit a permanent failure (invalid_number, error, undelivered, etc.) that retries can’t recover from, or
  • Failed with retries disabled.
It does not fire when a contact picks up, regardless of conversation outcome — that’s a successful delivery as far as the platform is concerned.

Channels

Both channels are independent. Either or both may be configured per campaign — at least one is required for escalation to fire.

Email

A short HTML message is sent via your configured email pipeline (Resend) to the address you set. Subject:
[Acme Onboarding Campaign] Contact escalated — *********4321
Body:
A contact in campaign Acme Onboarding Campaign has exhausted its retry attempts and was flagged for escalation.
ContactJohn Doe <+919876543210>
Last outcomeno_answer
Attempts3
Campaign ID47
Run ID1029
Timestamp2026-04-20 13:02:14 IST
Action: reach out to this contact manually or investigate why retries failed.
The timestamp is rendered in the timezone configured in your campaign’s Schedule step (defaults to Asia/Kolkata if unset). The contact’s phone number is masked to its last 4 digits in the subject; the full number appears in the body.

Webhook

If a webhook URL is set, a JSON payload is POSTed to it with a 10-second timeout:
{
  "event": "campaign.contact_escalated",
  "campaign_id": 47,
  "campaign_name": "Acme Onboarding Campaign",
  "run_id": 1029,
  "contact_id": 12891,
  "contact_phone": "+919876543210",
  "contact_name": "John Doe",
  "outcome": "no_answer",
  "attempt": 3,
  "timestamp": "2026-04-20T07:32:14Z",
  "timestamp_local": "2026-04-20 13:02:14 IST",
  "timezone": "Asia/Kolkata"
}
timestamp is always UTC ISO-8601 for machine consumers; timestamp_local and timezone mirror what humans see in the email. A response with status 2xx is treated as a successful delivery. Non-2xx responses are logged but don’t retry — the platform fires escalation exactly once per contact.

Configuration

In the campaign modal, Advanced Settings → Escalation:
☑ Escalation
   Email recipient            [ oncall@yourcompany.com           ]
   Webhook URL                [ https://you.com/hooks/campaign   ]
                              POSTs JSON: {event, campaign_id, ...}

   At least one is required for escalation to fire. Both will be tried independently.
Persisted shape in config:
{
  "escalation": {
    "enabled": true,
    "escalateEmail": "oncall@yourcompany.com",
    "escalateWebhookUrl": "https://you.com/hooks/campaign"
  }
}
Legacy fields email and escalateTo are still read by the backend for backward compatibility.

Counter

Each successful escalation dispatch (at least one channel accepted the call) increments escalated_count on the campaign run. You can see this in the campaign results panel and via the API:
curl https://api.thinnest.ai/campaigns/campaign_123/results \
  -H "Authorization: Bearer YOUR_API_KEY"
{
  "campaign_id": "campaign_123",
  "status": "completed",
  "total_contacts": 500,
  "sent": 498,
  "delivered": 485,
  "failed": 13,
  "escalated": 11
}

End-to-end flow

  1. Campaign starts → contacts dialed in batches.
  2. A contact’s first attempt returns no_answer.
  3. Retry #1 is scheduled for intervalsMinutes[0] minutes from now.
  4. Retry runs at the scheduled time, returns busy.
  5. Retry #2 is scheduled for intervalsMinutes[1] minutes after retry #1.
  6. Retry #2 runs, returns voicemail.
  7. There are no more retries (list length = 3, this was retry #3 candidate but the schedule only had 3 entries → stop).
  8. Escalation fires — email is sent + webhook POSTed.
  9. escalated_count on the run goes up by 1.
If the contact picks up at any retry, the chain ends and no escalation fires.

Best practices

  • Use escalating delays (60, 300, 1440) rather than aggressive uniform delays. Calling someone six times in an hour is a fast way to become spam.
  • Set both email + webhook so a missed email (filter, vacation) doesn’t mean a missed lead.
  • For high-volume campaigns, point the webhook at a real ticketing system (Zendesk, HubSpot, Slack) so escalations land in your team’s queue.
  • Use the campaign’s Schedule timezone rather than relying on UTC for the email timestamp — operators reading the email shouldn’t have to do mental math.