Graine AI

Place a call

Place one outbound call now, or schedule it for later.

Place one outbound call now, or schedule it for later.

The response always carries execution_id — the id the call runs under for its entire lifetime, minted here BEFORE anything can fail. That is what makes an unconfirmed dial safe: if the dial is not confirmed before this request returns, the call may already be live, and a client that redials would ring the customer twice. Poll GET /v2/calls/{execution_id} instead; the id is already yours.

The same id is sent in the X-Execution-Id response header — on every 201 and 202, and on any error raised once the request has been authenticated, authorized and its body parsed (an unusable phone number, an unknown agent, an empty wallet, no free line, a refused dial) — so even a failed request names the call it was about. A 401, 403, 422 or request-rate 429 is answered before any id exists and carries none.

Immediate (no scheduled_at) — 201, status: "queued". The dial is wrapped in your organization's concurrency gate, so a burst of requests queues against your line limit rather than overrunning it.

Queued for a line — 202, status: "scheduled", reason: "waiting_for_a_free_line". When every line your organization may use is busy, or the call cannot be placed at that moment, an immediate call is not refused: it is armed under the id you were given and placed automatically, within the agent's calling hours. scheduled_at is when it will next be tried; a call waiting for a line is dialled the moment one frees, which can be sooner. Do not resend it. Only if the call cannot be queued at all do you get the old answer — a 429 naming the limit with Retry-After: 5 (busy lines), or a 503/504 (calling is temporarily unavailable).

Outside the agent's calling window — 202, status: "scheduled", deferred_by_calling_guard: true. An immediate call the agent may not place right now is booked for a time inside its next open window instead of being refused; scheduled_at says when, and calling_window names the hours. A scheduled_at you send that falls outside the window is moved the same way.

Scheduled (scheduled_at set) — 202, status: "scheduled". The call is stored durably and armed: it is placed at its time even if the platform restarts in between. Cancel it any time before it dials with POST /v2/calls/{execution_id}/stop. It is listed in GET /v2/calls from this moment, not from the moment it rings, so a call waiting for its window is visible in the call history like any other. Until it dials it reads status: "scheduled"; if its window has closed when it comes due it stays scheduled with a new scheduled_at.

Unconfirmed — 202, status: "queued", dispatch: "pending_confirmation". The dial was sent but its confirmation did not come back in time. The call may or may not be live. Poll; do not redial.

402 — error 1400: your organization's credit balance cannot fund the call. An immediate call is refused by the credit check made as it dials, and a scheduled_at request is refused before anything is booked. The message wording differs between the two; branch on the code.

409 — error 1205: the number is on your do-not-call list. Nothing is dialled, queued or booked. Error 1202: the agent is switched off, so it takes no new calls; error 1203: the agent was deleted. Nothing is dialled, queued or booked. Switch the agent on and send the request again. Error 1204: a workflow's "Before a call is placed" holds this agent's calls, and the number has had its calls for now, and the call could not be queued. Nothing is dialled; Retry-After (seconds) and the message say when it can be called again.

Held by the call limit — 202, status: "scheduled", reason: "held_by_call_limit". A workflow's "Before a call is placed" holds this agent's calls and the number has had its calls for now: the call is queued under the id you were given and placed automatically at scheduled_at, when the person can be called again. A call sent with scheduled_at is held the same way when its time comes.

variables fills the {tokens} in your agent's prompt and welcome message. metadata is merged over it, so a key in both takes the metadata value. The merged object is what GET /v2/calls/{id} returns as variables from the moment this response is sent.

Starting language — variables.agent_language starts the call in one of an agent's languages: its code, such as "ta"; case does not matter. On an agent with two or more languages, a code it doesn't have is a 400 (error 1000) whose message lists the agent's languages, and nothing is dialled, queued or booked. Leave it out, or empty, to start in the agent's default language. On an agent with one language it is an ordinary variable.

Rate limit: 1200 requests per minute per organization, shared by the endpoints that place, stop and transfer calls and create campaigns. Exceeding it returns 429 with Retry-After; the X-RateLimit-* response headers report your remaining allowance on every call.

POST
/v2/calls
/v2/calls

The Authorization access token

Authorization

Authorization
Required
Bearer <token>

Your Graine API key. Create one in the dashboard under Developers, or via POST /v2/api-keys. Send it as Authorization: Bearer <key>.

In: header

Request Body

application/jsonRequired

agent_id
Required
Agent Id

The agent that will conduct the call. Must belong to your organization. If it carries a calling window, that window decides when this call may ring — see scheduled_at.

Minimum length: 1

to_number
Required
To Number

Recipient in E.164 format, e.g. +919812345678 (7-15 digits).

Minimum length: 1

from_numberFrom Number | null

Caller ID in E.164 format. Omit to have Graine pick one of the numbers registered to your organization.

variablesVariables

Prompt variables. Every {token} in the agent's prompt and welcome message is filled from here.

metadataMetadata

Additional context merged OVER 'variables' — on a key present in both, the value here wins.

scheduled_atScheduled At | null

ISO-8601 timestamp WITH a UTC offset (+05:30, -08:00, or Z). Must be at least 2 minutes and at most 30 days ahead. Omit to place the call immediately.

If the agent carries a calling window, a time outside it is NOT refused — the call is moved to a time INSIDE the next open window, and the 202 reports that time in scheduled_at with deferred_by_calling_guard: true. So this field is a request, and the response is the answer. The same applies when you omit it: an immediate call outside the window is scheduled rather than dialled.

The time is spread across the window, not stacked on its opening minute: ten calls held overnight against 09:00-21:00 land at ten different times through the day. It is derived from the execution id, so it is stable — re-reading the call returns the same time, and it never falls before the window opens or in its last five minutes.

recordRecord

Record the call audio. Applies to a scheduled call too — it is stored with the call and used at the moment it dials.

Default: true

timeout_secondsTimeout Seconds

How long to let the recipient's phone ring before giving up. Applies to a scheduled call too.

Default: 30Minimum: 5Maximum: 120

Query Parameters

organization_idOrganization Id

Optional. Must equal your key's organization.

Response Body

201

Call accepted and being placed.

execution_id
Required
Execution Id

Track the call with this id for its whole life.

status
Required
Status

Always "queued".

call_sidCall Sid | null

Provider-side leg id, for support tickets only.

agent_id
Required
Agent Id

The agent conducting the call.

to_number
Required
To Number

Recipient dialled.

from_numberFrom Number | null

Caller ID used.

created_atCreated At | null

When the call was accepted.

202

Either the call is scheduled for later (status "scheduled", with the time it will ring — this includes an immediate call that was queued because no line was free, which also carries reason "waiting_for_a_free_line"), or the dial was accepted and not confirmed in time (status "queued", dispatch "pending_confirmation"). Never redial on this status — the execution_id is already yours; poll it.

response
Required
Response 202 Create Call V2 Calls Post

400

1000 — malformed or contradictory input. 1002 — a required parameter is missing.

error
Required
Error

Stable integer code from the /v2 error table. Branch on this.

message
Required
Message

One human-readable sentence. Wording may change; the code will not.

401

1100 — missing, unknown or inactive API key. 1101 — a browser session token was presented instead of an API key.

error
Required
Error

Stable integer code from the /v2 error table. Branch on this.

message
Required
Message

One human-readable sentence. Wording may change; the code will not.

402

1400 — your credit balance cannot fund this call. Top up and retry.

error
Required
Error

Stable integer code from the /v2 error table. Branch on this.

message
Required
Message

One human-readable sentence. Wording may change; the code will not.

403

1102 — the key is valid but lacks the scope this endpoint requires, or names another organization. GET /v2/scopes reports what a key holds.

error
Required
Error

Stable integer code from the /v2 error table. Branch on this.

message
Required
Message

One human-readable sentence. Wording may change; the code will not.

404

1200 — no such resource in your organization. A resource belonging to another organization answers 404, never 403.

error
Required
Error

Stable integer code from the /v2 error table. Branch on this.

message
Required
Message

One human-readable sentence. Wording may change; the code will not.

409

1202 — the agent is switched off, so it takes no new calls: switch it on and try again. 1203 — the agent was deleted, so it takes no new calls.

error
Required
Error

Stable integer code from the /v2 error table. Branch on this.

message
Required
Message

One human-readable sentence. Wording may change; the code will not.

422

1001 — the body or query failed validation; the message names the first offending field.

error
Required
integer

Stable integer code from the error table. Branch on this, not on the message.

message
Required
string

One human-readable sentence. Wording may change; the code will not.

429

1300 — the per-organization request rate or concurrent-call limit was reached. Carries Retry-After.

error
Required
Error

Stable integer code from the /v2 error table. Branch on this.

message
Required
Message

One human-readable sentence. Wording may change; the code will not.

503

1501 — a service this endpoint depends on is unreachable or returned a 5xx.

error
Required
Error

Stable integer code from the /v2 error table. Branch on this.

message
Required
Message

One human-readable sentence. Wording may change; the code will not.

504

1502 — a service this endpoint depends on did not answer in time.

error
Required
Error

Stable integer code from the /v2 error table. Branch on this.

message
Required
Message

One human-readable sentence. Wording may change; the code will not.

curl -X POST "https://api.graine.ai/v2/calls?organization_id=string" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "6c6f4cc3-f56c-479d-8422-7f76694daa29",
    "to_number": "+919812345678",
    "from_number": "+917971442184",
    "variables": {
      "callee_name": "Raju",
      "scheme": "Bullet Mega Cistern"
    },
    "metadata": {
      "crm_lead_id": "LD-88421"
    },
    "scheduled_at": "2027-03-02T09:30:00+05:30",
    "record": true,
    "timeout_seconds": 30
  }'

Call accepted and being placed.

{
  "execution_id": "dbcffa37-92a1-4ae7-9814-71ca7b8ab3d3",
  "status": "queued",
  "call_sid": "0d76fa8c-ea49-4d78-9e5b-1a15ab7b3ac2",
  "agent_id": "6c6f4cc3-f56c-479d-8422-7f76694daa29",
  "to_number": "+919812345678",
  "from_number": "+917971442184",
  "created_at": "2026-08-26T12:19:54.580Z"
}