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 while status is queued or scheduled; otherwise null.
  • attempts - dial attempts made so far.
  • callId - the call (intr_…) of the attempt being dialled while dialing / in_progress, and of the last attempt once finished. It is null while the nudge is queued or scheduled - including between retries - and stays null if the nudge finishes without being dialled again (for example a retry that expired). Every attempt’s call is in attemptsList on Get a nudge.
  • statusReason - why it is held, or how it ended. Always a stable snake_case code (see statusReason codes). blockReason - the compliance gate that blocked it (for example dnd).
  • livemode - false for 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" with nextAttemptAt, because you set a future scheduledAt.

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.