Nudges
One on-demand outbound call to one number, with a lifecycle you can follow.
A nudge is one outbound AI call to one phone number, placed on demand by your backend (for example “remind this customer their payment is due”). Unlike a plain POST /v1/calls, a nudge has a lifecycle: it can be scheduled, held until the calling window opens, retried when nobody answers, and it always ends in a single result you can read or receive by webhook.
Use a nudge when you want OyeHello to own the “when and how often” of one call. Use POST /v1/calls when you want an immediate dial and will handle holds and retries yourself. Use a campaign for a list of people.
You can also fire and monitor nudges from the console → Nudges.
The Nudge object:
{
"id": "ndg_…",
"agentId": "agt_…",
"status": "scheduled",
"result": null,
"statusReason": "outside_calling_window",
"blockReason": null,
"toNumber": "+919812345678",
"fromNumber": "+918050000000",
"externalRef": "invoice-1042",
"variables": { "name": "Nalin", "amount": "4999" },
"metadata": { "orderId": "ORD-42" },
"outsideWindow": "queue",
"scheduledAt": null,
"nextAttemptAt": "2026-10-03T03:30:00Z",
"expiresAt": "2026-10-03T12:00:00Z",
"retry": { "maxRetries": 0, "on": ["no_answer", "busy"], "delayMinutes": 30 },
"attempts": 0,
"callId": null,
"livemode": true,
"createdAt": "2026-10-02T16:00:00Z",
"finishedAt": null
}
nextAttemptAt- when it will next be tried. Set only whilestatusisqueuedorscheduled; otherwisenull.attempts- dial attempts made so far.callId- the call (intr_…) of the attempt being dialled whiledialing/in_progress, and of the last attempt oncefinished. It isnullwhile the nudge isqueuedorscheduled- including between retries - and staysnullif the nudge finishes without being dialled again (for example a retry that expired). Every attempt’s call is inattemptsListon Get a nudge.statusReason- why it is held, or how it ended. Always a stablesnake_casecode (see statusReason codes).blockReason- the compliance gate that blocked it (for examplednd).livemode-falsefor nudges created with a test key.
Create a nudge
POST /v1/nudges · scope nudges:write · rate limited
curl -X POST https://api.oyehello.ai/v1/nudges \
-H "Authorization: Bearer ohk_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: invoice-1042-nudge-1" \
-d '{
"agentId": "agt_…",
"toNumber": "+919812345678",
"externalRef": "invoice-1042",
"variables": { "name": "Nalin", "amount": "4999" },
"retry": { "maxRetries": 1, "delayMinutes": 60 }
}'
Body
| Field | Type | Required | Notes |
|---|---|---|---|
agentId |
string | yes | A published, outbound call agent. |
toNumber |
string | yes | An Indian +91 subscriber number. Local formats are normalized. |
fromNumber |
string | no | Caller ID - an active number you own (400 from_number_not_owned otherwise); local formats are normalized. Defaults to your first active number. If it is no longer an active number of yours when the nudge is dialled, the nudge finishes failed with statusReason: "from_number_unavailable" - it never calls from a different number. |
externalRef |
string | no | Your own id for this nudge (1-120 chars), for example an order or cart id. Filterable on the list endpoint. It stays on the nudge - it is never written onto the contact. |
variables |
object | no | Values for the agent’s {variables} on this call. They override the values saved on the contact for this call only: the contact’s saved data is not changed, and a contact created by the nudge starts with its number only. Variables the agent’s flow requires must be supplied here or already saved on the contact. Up to 50 variables; names are letters, digits and _ (not starting with a digit, at most 64 characters); values are strings of up to 120 characters (the most the agent’s prompt takes from one variable - never silently shortened), numbers or booleans (stored as text); 16 KB in all. POST /v1/calls and /v1/scheduled-calls announce the same limits but, for now, accept over-limit variables and report them in warnings. Otherwise 400 invalid_variables with the offending name in meta.key. |
metadata |
object | no | Your own data, returned on the nudge and in its webhooks. Same limits as calls: up to 20 keys, 4 KB in all. The agent never sees it. |
scheduledAt |
ISO 8601 | no | Dial no earlier than this. Omitted (or now) means dial immediately. Cannot be in the past or at/after expiresAt. |
outsideWindow |
queue | reject |
no | Default queue. What to do if it is due now but outside the calling window. |
expiresAt |
ISO 8601 | no | Give up after this. Default 24 hours from now; at most 72 hours. A scheduledAt more than 24 hours ahead needs an expiresAt after it (else 422 invalid_schedule). |
retry.maxRetries |
integer | no | 0-2. Default 0 (no retries). |
retry.on |
array | no | Which unanswered outcomes to retry: no_answer, busy. Default both. |
retry.delayMinutes |
integer | no | 10-240. Default 30. |
Unknown fields are refused, never ignored: a misspelt scheduled_at or retry.max_retries returns 400 unknown_field with the field names in meta.fields.
Rate limit
Creates are limited per workspace: 60 per minute by default (OyeHello can raise it for your workspace). Every create response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; over the limit you get 429 rate_limited with Retry-After. Separately, a workspace can hold at most 10,000 pending (queued or scheduled) nudges per mode (429 pending_limit).
Idempotency
Idempotency-Key is required (up to 255 characters; longer is 400 invalid_idempotency_key). A retry with the same key and body returns the first response instead of creating a second nudge. Keys last 24 hours and are scoped to your workspace. A refused request (any 4xx) frees its key.
The nudge and the stored response are saved together, so a request that failed (any 4xx or 5xx) stored nothing and frees its key - retry it as is. If you retry with the same key while the first request is still running, you get 409 idempotency_in_progress; retry a moment later and you receive the first request’s stored result.
Timing
POST /v1/nudges never dials inside the request: it stores the nudge and responds at once. A nudge that is due now is picked up by the dialler within moments - follow it with GET /v1/nudges/{id} or the nudge.* webhooks.
One live nudge per number
While a nudge to a number is queued, scheduled, dialing or in_progress, creating another nudge to the same number returns 409 nudge_in_flight, with the existing nudge’s id in meta.nudgeId. Test-mode and live nudges are counted separately.
Response - 201
The response is { "data": <Nudge> }:
- Queued -
status: "queued",attempts: 0,callId: null: due now, about to be dialled. - Scheduled -
status: "scheduled"withnextAttemptAt, because you set a futurescheduledAt.
Lifecycle
status is where the nudge is now.
status |
Meaning |
|---|---|
queued |
Due now; about to be dialled (or waiting for a free dialler slot). |
scheduled |
Held until nextAttemptAt (future scheduledAt, calling window, retry delay, balance or lines). |
dialing |
The call has been placed: the callee picked up and the agent is being connected. (The carrier does not report a separate “ringing” moment, so a nudge stays queued while the phone rings.) |
in_progress |
The callee answered; the call is live. |
finished |
Terminal. result is set. |
result is set only when status is finished.
result |
Meaning |
|---|---|
connected |
The callee answered and spoke with the agent. |
voicemail |
The call was answered by voicemail. |
opted_out |
The callee asked not to be called again (they are added to your do-not-call list). |
no_answer |
Nobody picked up and no retries remain. |
busy |
The line was busy and no retries remain. |
failed |
Could not be completed: an invalid number (statusReason: "invalid_number"), a callee rejection such as SIP 603 (rejected - never redialled), a call that connected but the agent failed to start (not_connected), a carrier error, or outside_calling_window with outsideWindow: "reject". |
blocked |
A compliance gate refused the call. blockReason names the gate. |
expired |
expiresAt passed before it was ever dialled. |
cancelled |
You cancelled it. |
For a connected nudge, outcome (on the attempt, see below) is the agent’s disposition for the call, for example interested or callback_requested. It appears once the call’s post-call analysis finishes, a short while after the call ends. The call is also sorted onto the agent’s outcome board in the console, like any other call of that agent.
statusReason codes
While held (scheduled) |
Meaning |
|---|---|
outside_calling_window |
Waiting for the calling window to open. |
insufficient_balance |
Waiting for wallet balance (rechecked every 15 minutes). |
concurrency_full |
All your lines are busy. |
provider_error |
A temporary provider or platform error; retried shortly. |
retry |
An unanswered attempt; the next attempt is due at nextAttemptAt. |
not_connected |
The call connected but the agent did not start; redialled per retry. |
| When finished | Meaning |
|---|---|
A call end cause - agent_ended, caller_hangup, silence_timeout, max_duration, transfer_handoff, api_hangup, error, no_answer, busy |
How the last call ended. |
invalid_number, rejected, sip_<status>, not_connected |
The dial did not connect (see failed above). |
possibly_placed |
Placement failed at or after the dial; the callee may have been rung, so it is never redialled. |
from_number_unavailable |
fromNumber was no longer an active number of yours at dial time. |
india_residency_unavailable |
India-resident voice providers are not configured for the workspace. |
agent_not_found, agent_not_published, agent_not_call, agent_not_outbound, missing_required_variables, flow_not_ready, workspace_suspended, kyb_required, no_caller_id |
Refused at dial time (the agent, workspace or numbers changed after creation). |
placement_refused |
Refused at dial time for another reason. |
call_record_missing, unsettled |
The call’s record was lost, or the call never settled; the nudge was closed. |
expired, cancelled, erased |
Expired, cancelled by you, or cancelled by a data-erasure request. |
api_key_revoked |
Cancelled because the API key that created it was revoked. |
outside_calling_window, insufficient_balance, concurrency_full, provider_error |
(With a dialled nudge’s last result) why a pending retry could not happen. |
unknown |
Anything else. |
Calling window
A nudge is only dialled inside the agent’s compliance-pack calling hours. On top of that, no dial starts at or after 20:55 IST, so no call begins at 21:00. A nudge held for the window is released from the next calling-window opening - 09:00 IST after the 20:55 cutoff, or the start of your agent’s compliance-pack window - spread over 15 minutes so the opening does not spike.
With outsideWindow: "queue" (default) a nudge due outside the window is scheduled for the next opening, unless that is after expiresAt, in which case it finishes as expired. With outsideWindow: "reject" creation fails with 422 outside_calling_window and nextWindowAt in the error details.
Retries and expiry
By default a nudge is dialled once. Set retry to redial an unanswered attempt that ended in no_answer or busy, up to maxRetries extra times, delayMinutes apart. Answered calls, opt-outs, blocks and rejections (SIP 603) are never retried. A retry that would land after expiresAt is not scheduled; the nudge finishes with its last result instead. The same applies when a scheduled retry later cannot happen - it is held past expiresAt (calling window, balance or lines), or outsideWindow is reject and the retry falls outside the window: the nudge finishes with its last attempt’s result (for example no_answer), and statusReason says why it was not retried (outside_calling_window, insufficient_balance, concurrency_full, provider_error, or expired). A nudge that was dialled never finishes expired.
A call that connected but then failed to start the agent (statusReason: "not_connected") counts as an attempt, because the callee may have picked up. It is redialled only while maxRetries allows (delayMinutes later, held with statusReason: "not_connected"); otherwise the nudge finishes failed with statusReason: "not_connected".
Holds for an empty wallet (insufficient_balance, rechecked every 15 minutes), full lines (concurrency_full) or a provider error (provider_error) do not count as attempts. A workspace configuration problem that will not fix itself - India-resident voice providers not configured (india_residency_unavailable) - fails the nudge at once instead of holding it. A nudge still waiting at expiresAt finishes as expired.
List nudges
GET /v1/nudges · scope nudges:read
| Query | Notes |
|---|---|
status, result |
Filter by status or result. |
agentId, externalRef, toNumber |
Exact filters. |
createdAfter, createdBefore |
ISO 8601 timestamps. |
limit |
1-100, default 50. |
cursor |
The nextCursor of the previous page. |
Returns { "data": [<Nudge>…], "nextCursor": "…" }, newest first. nextCursor is null when a page has fewer than limit rows; a full last page may be followed by an empty page. Test keys see only test-mode nudges, and live keys only live ones.
Get a nudge
GET /v1/nudges/{id} · scope nudges:read
Returns the Nudge plus every attempt and the recording of the latest one:
{
"data": {
"id": "ndg_…",
"status": "finished",
"result": "connected",
"attempts": 1,
"attemptsList": [
{
"callId": "intr_…",
"startedAt": "2026-10-02T16:00:01Z",
"answeredAt": "2026-10-02T16:00:09Z",
"endedAt": "2026-10-02T16:01:40Z",
"endCause": "agent_ended",
"durationSeconds": 91,
"outcome": "interested",
"summary": "Customer will pay by Friday.",
"extracted": { "promisedDate": "2026-10-04" }
}
],
"recordingUrl": "https://…"
}
}
recordingUrl is a short-lived link, or null when there is no recording. outcome is null until the disposition is known.
The recording, summary and extracted are the call’s content, so they need the calls:read scope as well: with nudges:read alone, recordingUrl, summary and extracted are null. (The nudge.finished / nudge.attempt_ended webhook payloads carry summary and extracted, so a key with webhooks:read can read them from the delivery log.) Fetch the full transcript with GET /v1/calls/{callId} (also calls:read).
Cancel a nudge
POST /v1/nudges/{id}/cancel · scope nudges:write
Cancels a queued or scheduled nudge; it finishes with result: "cancelled" and sends nudge.finished. A nudge that is being dialled right now (the phone may be ringing) or is already dialing or in_progress cannot be cancelled, so the request returns 409 nudge_not_cancellable. End a live call with POST /v1/calls/{callId}/hangup. A nudge that already finished also returns 409.
Cancel many nudges
POST /v1/nudges/cancel · scope nudges:write
Cancel many not-yet-dialled nudges at once - for example after a bad import. Send either a list of ids or all: true (every queued / scheduled nudge of the key’s mode), not both (400 otherwise):
{ "ids": ["ndg_…", "ndg_…"] }
{ "all": true }
Returns { "data": { "cancelled": 2, "ids": ["ndg_…", "ndg_…"] } }. Nudges that are being dialled, live, finished, unknown or of the other mode are skipped. ids takes up to 1,000 ids. Each cancelled nudge sends nudge.finished (result: "cancelled").
Revoking an API key (console → Settings → API keys) cancels every queued or scheduled nudge that key created, with statusReason: "api_key_revoked" and a nudge.finished for each - a leaked key’s backlog stops with it. A nudge the dialler had already picked up is stopped just before its dial (same result); one whose call is already being placed is not stopped and reports that call as usual.
Preflight
POST /v1/nudges/preflight · scope nudges:read
Check a number before creating a nudge. Nothing is stored and nothing is dialled.
{ "agentId": "agt_…", "toNumber": "+919812345678" }
{
"data": {
"toNumber": "+919812345678",
"blocked": false,
"blockReason": null,
"insideWindow": false,
"nextWindowAt": "2026-10-03T03:30:00Z"
}
}
blocked means a compliance gate would refuse the call (blockReason names it). insideWindow is whether a dial could start right now, and nextWindowAt is when the window next opens.
Webhooks
Nudges send four webhook events, signed and retried like every other event. Subscribe to them on an endpoint, or leave events empty to receive all.
| Event | Fires when |
|---|---|
nudge.scheduled |
The nudge is being held for a future time (scheduled, window, balance, lines). Sent when a hold starts, when its reason changes, or when its time moves by more than an hour - not on every recheck of a full line or empty wallet. |
nudge.dialing |
A call was placed and answered; the agent is being connected. |
nudge.attempt_ended |
An attempt ended unanswered and a retry was scheduled. Adds an attempt object when a call row exists. |
nudge.finished |
The nudge reached a final result. Also carries the last attempt’s fields (callId, startedAt, answeredAt, endedAt, endCause, durationSeconds, outcome, summary, extracted) when there was a call. |
The payload data is the Nudge object (so nudge.dialing reports the attempt number as attempts). Each occurrence has a stable event id across retries and replays, so dedupe on it. Events can arrive out of order; reconcile with status and createdAt, or fetch the nudge.
When a person’s number is erased (data-erasure request), their pending nudges finish cancelled with statusReason: "erased" and send nudge.finished with toNumber: "[erased]" and no variables, metadata or external ref. Their earlier nudge.* deliveries are scrubbed the same way and can no longer be replayed (409 delivery_redacted).
Test mode
A nudge created with a test key never dials and costs nothing, and is returned with livemode: false. The calling window and your do-not-call list are still evaluated, so you see real scheduled and blocked behaviour. Magic numbers drive the other outcomes:
| Number ends in | Result |
|---|---|
0000 |
no_answer |
1111 |
busy |
2222 |
blocked (blockReason: "dnd") |
Compliance
Every nudge goes through the same checks as other calls. Your workspace do-not-call list, opt-outs and your agent’s compliance pack are enforced. The national DND register (NCPR) is not checked by OyeHello in this version, so you remain responsible for screening against it where it applies to your use case.
Errors
Errors are { "error": "<message>", "code": "<code>", "meta": { … } }; code and meta are present where listed.
| Status | Code | Meaning |
|---|---|---|
400 |
- | The body or query failed validation; error names the field (for example invalid toNumber: …). |
400 |
unknown_field |
The body has a field the API does not know (meta.fields). |
400 |
invalid_variables |
variables breaks a limit (meta.key, meta.detail). |
401 |
- | Missing, unknown, revoked or expired API key. |
403 |
api_scope_missing |
The key lacks the scope the endpoint needs (meta.scope). |
404 |
agent_not_found |
agentId is not an agent of this workspace. |
404 |
- | The nudge does not exist (or belongs to the other mode). |
400 |
idempotency_key_required |
The Idempotency-Key header is missing. |
400 |
invalid_idempotency_key |
The Idempotency-Key is longer than 255 characters. |
409 |
idempotency_in_progress |
A request with this key is still running. Retry later to get its stored result. |
422 |
idempotency_key_reused |
The key was used for a different request. |
400 |
from_number_not_owned |
fromNumber is not an active number this workspace owns. |
400 |
invalid_cursor |
cursor is not a nextCursor from this endpoint. |
422 |
invalid_number |
toNumber is not a valid Indian +91 number. |
422 |
invalid_schedule |
scheduledAt is in the past or after expiresAt, or expiresAt is not within the next 72 hours. |
422 |
outside_calling_window |
outsideWindow: "reject" and the call is due outside the window. |
422 |
missing_required_variables |
The agent’s flow needs variables you did not supply. |
422 |
agent_not_published |
The agent is not published. |
422 |
agent_not_outbound |
The agent is not an outbound call agent. |
422 |
agent_not_call |
The agent is a chat agent, not a call agent. |
422 |
flow_not_ready |
Something the agent’s flow uses is not connected. Reconnect it and retry. |
403 |
kyb_required |
The workspace must be verified (KYB) first. |
403 |
workspace_suspended |
The workspace is suspended. |
429 |
pending_limit |
Too many pending nudges for this workspace. |
409 |
nudge_in_flight |
A nudge to this number is already queued, scheduled or in progress. meta.nudgeId is its id. |
409 |
nudge_not_cancellable |
The nudge is being dialled, live, or finished. |
429 |
rate_limited |
Create rate limit exceeded. Retry after Retry-After seconds. |