> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tracklysms.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Hold a Send for Approval

> Store a send for a person to approve in the dashboard before the key sends it.

Holds a [single send](/api-reference/v2/messages/send-single) or a [bulk batch](/api-reference/v2/messages/send-bulk) until a person approves it. Nothing is sent by this call. The flow is:

1. The key calls this endpoint with the exact payload it would have sent. The response is the pending send with `status: pending` and an `expires_at` 24 hours out.
2. A person opens **Settings → Send Approvals** in the dashboard, reads the recipients, the message and your `note`, and approves or rejects it.
3. An authorized same-account, same-mode key [polls the pending send](/api-reference/v2/pending-sends/get) until `status` is `approved`, then calls [send](/api-reference/v2/pending-sends/send). The immutable review contains rendered recipients, senders, message text, and link destinations. Allocated message and short-link IDs use display placeholders. Rendering or configuration changes require a new review before publication.

A key created with **Require send approval** cannot send SMS, email, or OTP directly (`403 confirmation_required`), or perform send-enabling schedule operations; this endpoint is its way to send approved SMS. Any key can use it voluntarily. Check `api_key.send_policy.require_confirmation` on [Who Am I](/api-reference/v2/auth/whoami).

Creation validates shape, allowed lists, and the rendered review without transport or quota consumption. Suppression, double opt-in, warmup, and current key policy run on execution; billing remains downstream. A send held by a [sandbox](/api-reference/v2/sandbox) key is marked `sandbox: true` and can only be executed by a sandbox key, as a simulation; a live hold can only be executed by a live key.

Each account can have 100 unexpired open holds and create 500 holds in a rolling 24-hour window, shared across keys and modes. Rejection or cancellation does not refund the rolling creation limit. Admission atomically reserves capacity and the pending ID. The payload plus rendered review is limited to 4 MiB. Records contain recipient and message data and are subject to 30-day TTL retention; expiry after 24 hours prevents approval or execution but does not immediately delete the record.

Use `Idempotency-Key` for creation retries; MCP requires it. Within the 24-hour reservation window, the same account, mode, idempotency header value, and exact body recover the reserved hold; changed payload returns `409 idempotency_conflict`. On `unknown_hold`, read the pending ID when returned. A network timeout may omit it: replay the same header and body, then use a bounded [recent-hold lookup](/api-reference/v2/pending-sends/list) if needed. Never change the key or create a replacement send to resolve uncertainty.

## Body Parameters

Provide exactly one of `message` or `messages`.

<ParamField body="message" type="object">
  One [`POST /v2/send`](/api-reference/v2/messages/send-single) request body: `to`, `list_number`, `body`, and optionally `wrap_links`, `metadata`, `contact_data`, `linkOverrides`.
</ParamField>

<ParamField body="messages" type="object[]">
  A [`POST /v2/send/bulk`](/api-reference/v2/messages/send-bulk) batch of up to 1000 messages, held and approved as one unit.
</ParamField>

<ParamField body="note" type="string">
  Why this send should go out, shown to the approver next to the message. At most 500 characters.
</ParamField>

## Response Fields

<ResponseField name="id" type="string">Pending send ID; use it to poll, cancel, and send.</ResponseField>
<ResponseField name="status" type="string">`pending` on creation. Later `approved`, `executing`, `unknown_execution`, `sent`, `rejected`, `cancelled`, or `expired`. `sent` means submitted, not delivered.</ResponseField>
<ResponseField name="execution" type="object">Publication attempt ID, attempted and confirmed counts, and per-message evidence on detail responses; `null` before execution. Uncertain outcomes require reconciliation using the same pending ID.</ResponseField>
<ResponseField name="kind" type="string">`send` or `send_bulk`.</ResponseField>
<ResponseField name="recipient_count" type="integer">Messages in the held payload.</ResponseField>
<ResponseField name="note" type="string">The note as submitted, or `null`.</ResponseField>
<ResponseField name="api_key_id" type="string">The key that held it.</ResponseField>
<ResponseField name="sandbox" type="boolean">`true` when a sandbox key held it. Only a key in the same mode can execute it.</ResponseField>
<ResponseField name="payload" type="object">The held payload exactly as submitted: the `message` body, or `{ "messages": [...] }`. Object keys anywhere in it (for example in `metadata` or `contact_data.custom_fields`) cannot start with `$` or contain `.`.</ResponseField>
<ResponseField name="created_at" type="string">ISO-8601, UTC.</ResponseField>
<ResponseField name="expires_at" type="string">When an undecided or unexecuted send lapses (ISO-8601, UTC), 24 hours after creation. An expired send cannot be approved or sent.</ResponseField>
<ResponseField name="decided_at" type="string">When it was approved, rejected, or cancelled; `null` until then.</ResponseField>
<ResponseField name="decided_by" type="string">`user:<id>` for a dashboard decision, `api_key:<id>` for a cancel; `null` until then.</ResponseField>
<ResponseField name="decision_reason" type="string">The approver's rejection reason, or `null`.</ResponseField>
<ResponseField name="executed_at" type="string">When execution was claimed; `null` before a claim. This timestamp does not establish submission or delivery.</ResponseField>
<ResponseField name="executed_by_key_id" type="string">The key that executed it; `null` until then.</ResponseField>
<ResponseField name="result" type="object">The send response body once executed (`message_id` and `status` for a single send; `queued_count`, `error_count`, `errors` for a batch); `null` before.</ResponseField>
<ResponseField name="last_error" type="object">The error body from the most recent failed send attempt, or `null`.</ResponseField>

## Errors

| HTTP Status | Error Code                                                                                                          | Description                                                                                                                                                                    |
| ----------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 400         | `invalid_payload`                                                                                                   | Not a JSON object, neither or both of `message` / `messages` given, `note` is too long, or an object key in the payload starts with `$` or contains `.`.                       |
| 400         | `missing_to`, `missing_list_number`, `missing_body`, `invalid_phone`, `invalid_list_number`, `invalid_contact_data` | A message fails the same shape checks as `/v2/send`; for a batch the message index is named in `error`.                                                                        |
| 400         | `list_not_found`                                                                                                    | A `list_number` is not an active sending list on this account.                                                                                                                 |
| 400         | `missing_messages`, `invalid_messages`                                                                              | `messages` is empty or not an array.                                                                                                                                           |
| 403         | `list_not_allowed_for_key`                                                                                          | A `list_number` is outside the key's `send_policy.list_numbers`.                                                                                                               |
| 413         | `too_many_messages`                                                                                                 | More than 1000 messages in `messages`.                                                                                                                                         |
| 413         | `pending_send_too_large`                                                                                            | Payload plus rendered review exceeds 4 MiB.                                                                                                                                    |
| 409         | `idempotency_conflict`                                                                                              | The same creation key was used with a different payload.                                                                                                                       |
| 429         | `too_many_pending_sends`                                                                                            | 100 unexpired open holds or 500 holds in the rolling 24-hour window. `reason` distinguishes `open_holds` from `holds_24h`; `retry_after` and `Retry-After` give a retry delay. |
| 503         | `pending_send_unavailable`                                                                                          | Admission authority is unavailable.                                                                                                                                            |
| 503         | `unknown_hold`                                                                                                      | Persistence is uncertain; read the ID when present or replay the same `Idempotency-Key` and exact body.                                                                        |

Also `401 invalid_credentials`. Supports the [`Idempotency-Key`](/api-reference/v2/idempotency) header.

## Examples

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST https://api.tracklysms.com/api/v2/pending-sends \
    -H "X-Api-Key: trk_your_api_key" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: reservation-confirmation-1042-v1" \
    -d '{
      "message": {
        "to": "+14155551234",
        "list_number": "+18005551234",
        "body": "Hi Sam, your table for 4 is confirmed for 7pm tonight. Reply STOP to opt out."
      },
      "note": "Reservation confirmation requested in chat at 2:10pm"
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json Held (202) theme={null}
  {
    "id": "66f1c2a9b8e4d3f0a1b2c3d4",
    "status": "pending",
    "kind": "send",
    "recipient_count": 1,
    "note": "Reservation confirmation requested in chat at 2:10pm",
    "api_key_id": "66e0a1b2c3d4e5f6a7b8c9d0",
    "sandbox": false,
    "payload": {
      "to": "+14155551234",
      "list_number": "+18005551234",
      "body": "Hi Sam, your table for 4 is confirmed for 7pm tonight. Reply STOP to opt out."
    },
    "created_at": "2026-09-04T14:10:32.000000Z",
    "expires_at": "2026-09-05T14:10:32.000000Z",
    "decided_at": null,
    "decided_by": null,
    "decision_reason": null,
    "executed_at": null,
    "executed_by_key_id": null,
    "result": null,
    "last_error": null
  }
  ```

  ```json Too many open sends (429) theme={null}
  { "error": "This account already has 100 sends awaiting approval or execution", "code": "too_many_pending_sends" }
  ```
</ResponseExample>
