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

# API reference

> Base URL, request shape, response shape, and the fields every endpoint shares.

## Base URL

```
https://api.bulkneo.com
```

All requests are HTTPS and all bodies are JSON.

<Note>
  If you bought BulkNeo through a reseller, your API address may be different. Your portal shows the address that applies to your account — use that one. Everything else on this site is identical.
</Note>

## Every request needs

<ParamField header="apikey" type="string" required>
  Your API key. See [Authentication](/authentication).
</ParamField>

<ParamField header="Content-Type" type="string">
  `application/json` on any request with a body.
</ParamField>

<ParamField body="instance_id" type="uuid" required>
  Which of your connected numbers to send **from**. Copy it from your numbers screen in the portal, or list them with [`GET /v1/instances`](/api-reference/instances/list). It must belong to your account.
</ParamField>

<ParamField body="to" type="string" required>
  The recipient, with country code and no leading zero — for example `919876543210`. Spaces, dashes, brackets and a leading `+` are accepted and stripped; 6 to 15 digits must remain.

  Not used by [`POST /v1/numbers/check`](/api-reference/numbers/check), which takes a `numbers` array instead.
</ParamField>

Unknown extra fields in a body are ignored rather than rejected, so adding your own bookkeeping field to a request will not break it — but nothing will be done with it either.

## One path per message type

There is no single `/messages` endpoint with a `type` field. Each kind of message has its own path:

```
POST /v1/messages/text
POST /v1/messages/image
POST /v1/messages/document
…
```

On each path every field is either always required or always optional — there is no "caption is allowed unless the type is audio" matrix to learn.

## Every response

Success:

```json theme={null}
{
  "success": true,
  "data": { "…": "…" },
  "request_id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
}
```

Failure:

```json theme={null}
{
  "success": false,
  "error": {
    "code": "instance_not_connected",
    "message": "This WhatsApp instance is not connected. Connect it before sending messages."
  },
  "request_id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
}
```

`request_id` is on every response either way. Log it — it is what support needs to find your request.

## What a send returns

<ResponseField name="data.instance_id" type="uuid">
  The number the message was sent from.
</ResponseField>

<ResponseField name="data.to" type="string">
  The recipient, as we normalised it.
</ResponseField>

<ResponseField name="data.type" type="string">
  Echoes the message type you sent.
</ResponseField>

<ResponseField name="data.status" type="string">
  Always `sent` — meaning accepted for delivery.
</ResponseField>

<ResponseField name="data.message_id" type="string | null">
  The message's own id. **Keep it** — it is what you [quote](/guides/replies) or [react to](/api-reference/messages/reaction) later.
</ResponseField>

<ResponseField name="data.provider_status" type="string | null">
  The delivery state at the moment of sending, for example `PENDING`. A snapshot, not a delivery receipt — there is no way to ask later whether a message was read.
</ResponseField>

<Warning>
  A `200` means WhatsApp accepted the message for delivery. It does not promise the recipient received it, read it, or — for [buttons and lists](/guides/interactive-messages) — that it rendered the way you designed it.
</Warning>

## Replying to a message

Any send except a reaction accepts two optional fields that turn it into a reply:

<ParamField body="quoted_message_id" type="string">
  The `data.message_id` of the message being replied to.
</ParamField>

<ParamField body="quoted_from_me" type="boolean" default="false">
  `true` when the quoted message is one you sent from this number. Only valid together with `quoted_message_id`.
</ParamField>

See [Replying to a message](/guides/replies).

## Rate limits

Per minute, per API key. There are **two separate budgets**, and every response says which one it is describing in `X-RateLimit-Scope`:

| Scope       | Covers                                                       | Allowance                        |
| ----------- | ------------------------------------------------------------ | -------------------------------- |
| `messages`  | `/v1/messages/*` and `/v1/numbers/check`, sharing one budget | From your plan — read the header |
| `instances` | `/v1/instances/*`                                            | 120 per minute                   |

Every rate-limited response also carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (seconds until your allowance goes back up), and a `429` adds `Retry-After`. Details on the [Errors](/errors#rate-limits) page.

<Warning>
  If you cache a limit, cache it against its scope. `X-RateLimit-Limit: 120` from an instance call is **not** your send allowance.
</Warning>

## Try it from this site

Every endpoint page below has a playground. Paste your API key and a real `instance_id` into it and it sends a **real request** to the live API.

<Warning>
  The playground is not a sandbox. There is no test mode: a send from this page sends a real WhatsApp message to the number you type. Use your own number while you are exploring.
</Warning>
