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

# Making requests

> Base URL, response format, IDs, pagination, tracing and versioning.

## Base URL and format

```text theme={null}
https://api.blujay.io/v1
```

Send JSON with `Content-Type: application/json` and authenticate with `Authorization: Bearer …` ([Authentication](/authentication.md)). Call the API from your server only: keys must never reach a browser or mobile app.

* **Timestamps** are ISO 8601 in UTC, for example `2026-10-14T17:00:00.000Z`.
* **Phone numbers** are E.164, for example `+15555550123`. Recipients must be US numbers.
* **Services** are `iMessage`, `RCS` and `SMS` in API responses. Webhook payloads use lowercase `imessage`, `rcs` and `sms`.
* **Unknown body fields** are rejected with [`VALIDATION_ERROR`](/errors/validation-error), and so is a body that isn't valid JSON.

> [!WARNING]
>
> **Common mistake:** in a query string, `+` means a space. URL-encode a phone number as `%2B15555550123`, for example `GET /v1/contacts/search?phone_number=%2B15555550123`. In a JSON body, write `+15555550123` as is.

## Responses

Every successful response has the same envelope. The result is in `data`:

```json theme={null}
{
  "status": "success",
  "data": { "chat_id": "chat_abc123" },
  "trace_id": "trc_4c8e2a91f0d7",
  "timestamp": "2026-10-14T17:01:00.000Z"
}
```

Errors use `status: "error"` and an `error` object with a stable `code`. Check `status` before you read `data`: an error response has no `data`. See [Errors](/errors.md).

| Status | Meaning |
| - | - |
| `200 OK` | Done. |
| `201 Created` | A resource was created: a contact, a saved contact card, an attachment, a webhook subscription or a workflow enrollment. |
| `202 Accepted` | Accepted for processing, for example a message send. The work finishes later; watch for it with [webhooks](/webhooks/overview.md). |
| `204 No Content` | Done, with no body, for example typing indicators and read receipts. |
| `4xx` | Something about the request needs to change. Retry only when `error.retryable` is `true`: that is every `429`, and a few `409`s such as [`IDEMPOTENCY_IN_PROGRESS`](/errors/idempotency-in-progress). Most `409`s are not retryable. |
| `5xx` | A problem on Blujay's side, or a change that couldn't be made. Retry with the same `Idempotency-Key` when `error.retryable` is `true`. |

## IDs

Resource IDs are opaque strings with a prefix that tells you the type. Store them as strings and don't parse them. (Credential IDs in `GET /v1/me` are plain UUIDs.)

| Prefix | Resource |
| - | - |
| `workspace_` | Workspace |
| `phone_` | Phone number |
| `chat_` | Chat (direct or group) |
| `msg_` | Message |
| `contact_` | Contact |
| `att_` | Attachment |
| `vcard_` | Saved contact card |
| `workflow_` | Workflow |
| `member_` | Team member |
| `whsub_` | Webhook subscription |
| `evt_` | Webhook event |
| `trc_` | Trace (one request) |

An ID with the wrong prefix for an endpoint returns [`INVALID_PUBLIC_ID`](/errors/invalid-public-id). IDs from another workspace return [`NOT_FOUND`](/errors/not-found).

## Pagination

Most list endpoints return a page of results and a cursor. These return everything at once, with no cursor: `GET /v1/phone-numbers`, `GET /v1/contacts/search` (up to `limit` matches), `GET /v1/workflows`, `GET /v1/team-members/assignable` and `GET /v1/webhook-subscriptions`.

```json theme={null}
{
  "status": "success",
  "data": {
    "data": [{ "chat_id": "chat_abc123" }, { "chat_id": "chat_def456" }],
    "has_more": true,
    "next_cursor": "eyJ2IjoxLCJrIjoi…",
    "limit": 25
  },
  "trace_id": "trc_1a2b3c4d5e6f",
  "timestamp": "2026-10-14T17:02:00.000Z"
}
```

* `limit` sets the page size: 1 to 100, default 25. Contact search is the exception: 1 to 25, default 10, with no cursor.
* While `has_more` is `true`, pass `next_cursor` as `cursor` to get the next page. Keep the same filters.
* Cursors are opaque and tamper-proof. A changed or foreign cursor returns [`INVALID_CURSOR`](/errors/invalid-cursor). Leave `cursor` out of the first request; a placeholder such as `cursor=string` is rejected the same way.

```bash theme={null}
curl "https://api.blujay.io/v1/chats?limit=50&cursor=eyJ2IjoxLCJrIjoi…" \
  -H "Authorization: Bearer blu_your_api_key"
```

## Tracing requests

* Every response includes `trace_id` in the body and an `X-Trace-ID` header.
* Send your own `X-Request-ID` header to correlate with your logs. Blujay echoes it back as `client_request_id` in error responses and in most success responses. The send responses (`POST /v1/chats`, `POST /v1/chats/{chat_id}/messages` and `POST /v1/messages/{message_id}/retry`) leave it out, so map your request ID to the returned `message.id` yourself there.

Log both. When you contact [support@blujay.io](mailto:support@blujay.io), include the `trace_id`, your `X-Request-ID` and the time of the request. Never include your API key.

## Retries and timeouts

Use a client timeout of about 30 seconds. If a write times out or returns a retryable error, retry it with the **same** `Idempotency-Key`, so a request that actually succeeded is never repeated. See [Idempotency](/idempotency.md) and [Rate limits](/rate-limits.md).

## Time zones and `reset_at`

Every timestamp is UTC. One limit follows a local clock: a number's new-contact count resets at midnight in your workspace's time zone, which the API doesn't expose. Don't compute the reset yourself. Read `limits.new_contacts.reset_at` on the number, or `error.details[0].reset_at` on a `429`, and wait until then. See [Phone numbers and limits](/phone-numbers.md#when-limits-reset).

## Versioning

The version is in the path: `/v1`. Within v1, Blujay works to keep a well-written client working.

**Changes Blujay can make at any time:**

* adding endpoints, optional request fields and response fields;
* adding error codes for new situations, webhook event types and enum values;
* rewording `error.message` and other text meant for people;
* adjusting sending limits and messaging-safety rules to protect delivery. These always use documented error codes, and Dedicated numbers report their current limits in [`GET /v1/phone-numbers`](/phone-numbers).

**Changes Blujay avoids:**

* removing or renaming an endpoint, field, error code or enum value;
* making an optional request field required;
* changing what a field means or its type;
* returning a different error code for the same situation;
* changing a webhook payload within a version you've pinned.

If one of these is ever needed, we'll email the admins of workspaces that use the API ahead of time, and the API reference will mark the affected item **Deprecated**. The only exception is a change needed right away for security or legal reasons.

Write your client to ignore fields it doesn't know and to handle unknown enum values. Branch on `error.code`, never on `error.message`. Webhook payload shapes are versioned separately by date, and a subscription stays on the version you pin. See [Webhook versions](/webhooks/versions.md).

## Next steps

> **[Idempotency](/idempotency.md)**
>
> Retry safely without sending twice.

> **[Rate limits](/rate-limits.md)**
>
> Request limits and how to back off.

> **[Phone numbers and limits](/phone-numbers.md)**
>
> Sending limits and the live fields that report them.

> **[Errors](/errors.md)**
>
> Every error code and whether it is retryable.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.