# Ingestion API overview

> Base URL, endpoints, API keys, limits, rate limits and error format of the Whisperr ingestion API at api.whisperr.net.

The Whisperr ingestion API takes your product events, user identities and consent records, and answers data requests about a user. Use this page when you call the API directly. If an SDK exists for your stack, use the SDK: it does the batching, retries and idempotency for you.

## Base URL

```text
https://api.whisperr.net
```

All paths on this page start with `/v1/`.

## Endpoints

| Endpoint                             | Key                   | Purpose                                                                                                                    |
| ------------------------------------ | --------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `POST /v1/events/batch`              | publishable or server | Send up to 500 events. See [Track events](/api/events/).                                                                   |
| `POST /v1/events/track`              | publishable or server | Send one event. See [Track events](/api/events/).                                                                          |
| `POST /v1/identify`                  | publishable or server | Set a user’s traits and contact channels. See [Identify users](/api/identify/).                                            |
| `POST /v1/consent/assertions`        | server only           | Record proof of a consent choice. See [Contact consent](/concepts/consent/#record-consent-proof-with-a-consent-assertion). |
| `GET /v1/users/{external_id}/export` | server only           | Export everything Whisperr stores about a user. See [Export and erase a user](/api/data-subjects/).                        |
| `DELETE /v1/users/{external_id}`     | server only           | Erase a user. See [Export and erase a user](/api/data-subjects/).                                                          |

Use `/v1/events/batch` for anything that sends more than one event. All SDKs use it. `/v1/events/track` is for one-off calls such as scripts and webhooks.

## Which API key do I use?

Whisperr has two kinds of ingestion key. Create them in the dashboard under **Settings → API keys**. Every workspace can create keys, also before the trial starts.

| Dashboard label | Prefix  | Where it runs     | What it can do                                                                                                                                                |
| --------------- | ------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| App             | `wpk_…` | Apps and browsers | Send events and identify calls. It ships inside your app, so anyone can read it.                                                                              |
| Server          | `wrk_…` | Your backend only | Everything the app key can do. It can also always opt a contact in, change a user’s existing email address, record consent proof, and export or erase a user. |

Keep the server key in your backend’s secrets. Never put it in client code. See [Contact consent](/concepts/consent/) for when the app key can opt a contact in, and what only the server key can do. See [API keys](/guides/api-keys/) to create, replace or revoke a key.

## How do I authenticate a request?

Send the key in one of these two headers. Both work on every endpoint.

```http
X-API-Key: wrk_your_server_key
```

```http
Authorization: Bearer wrk_your_server_key
```

A request without a key gets `401 missing_api_key`. A wrong or revoked key gets `401 invalid_api_key`.

## Send a first event

Put your server key in an environment variable, then send one event. The `occurred_at` and `$message_id` values are generated by the shell, so you can run the command more than once.

```bash
export WHISPERR_SERVER_KEY=wrk_your_server_key


curl https://api.whisperr.net/v1/events/track \
  -H "X-API-Key: $WHISPERR_SERVER_KEY" \
  -H "Content-Type: application/json" \
  -d @- <<EOF
{
  "external_user_id": "user_8842",
  "event_type": "payment_failed",
  "occurred_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
  "properties": { "amount_cents": 4900 },
  "context": { "\$message_id": "$(uuidgen)" }
}
EOF
```

The answer is `202 Accepted`:

```json
{ "event": { "mapping_status": "pending" }, "processing": "queued" }
```

`202` means Whisperr stored the request. It processes the event right after. See [Accepted, then processed](/api/delivery/#accepted-then-processed).

## Can I call the API from a browser?

Yes, for the three ingestion endpoints. `POST /v1/events/batch`, `POST /v1/events/track` and `POST /v1/identify` allow cross-origin requests from any origin, with the publishable key. The consent, export and erase endpoints need the server key and do not allow cross-origin requests.

## How strict is validation?

The Whisperr API rejects unknown fields. A misspelled field name fails with `400` instead of being ignored. This keeps every SDK on the same contract. In a batch, only the event with the problem is rejected. See [Response handling](/api/delivery/#response-handling).

## What are the size limits?

| Limit                                                  | Value                                                                   |
| ------------------------------------------------------ | ----------------------------------------------------------------------- |
| Body of `POST /v1/events/track` or `POST /v1/identify` | 1 MiB                                                                   |
| Body of `POST /v1/events/batch`                        | 10 MiB                                                                  |
| Events per batch                                       | 1 to 500                                                                |
| `properties` of one event, as JSON                     | 50 KB                                                                   |
| `traits` of one identify, as JSON                      | 50 KB                                                                   |
| `channels` per identify or event                       | 10                                                                      |
| `anonymous_id`                                         | 1 to 128 printable characters                                           |
| `$message_id`                                          | 200 bytes. A longer value is ignored, so the event is not deduplicated. |

A body over its limit gets `413 payload_too_large`.

## What are the rate limits?

Whisperr counts requests per API key in fixed one-minute windows. Every counted response carries these headers:

| Header                  | Meaning                                     |
| ----------------------- | ------------------------------------------- |
| `X-RateLimit-Limit`     | Requests this key may make in one window.   |
| `X-RateLimit-Remaining` | Requests left in the current window.        |
| `X-RateLimit-Reset`     | End of the current window, in Unix seconds. |

Over the limit, the answer is `429 rate_limited` with a `Retry-After` header in seconds. Wait that long, then retry. All browsers of one app share the publishable key, so they share its budget.

## What does an error look like?

Every error from the Whisperr API has the same JSON shape:

```json
{
  "error": {
    "code": "invalid_request",
    "message": "event_type must be snake_case",
    "request_id": "8d2f6a0c1b3e4f5a"
  }
}
```

Match on `code`. `message` is for people and can change.

| Status       | `code`                  | Meaning                                                      |
| ------------ | ----------------------- | ------------------------------------------------------------ |
| `400`        | `invalid_json`          | The body is not one JSON object, or it has an unknown field. |
| `400`        | `invalid_request`       | A field failed validation. `message` names it.               |
| `401`        | `missing_api_key`       | No key in `X-API-Key` or `Authorization`.                    |
| `401`        | `invalid_api_key`       | The key is wrong or revoked.                                 |
| `403`        | `server_key_required`   | Export and erase need the server key.                        |
| `404`        | `user_not_found`        | Export found no user with this id.                           |
| `404`        | `route_not_implemented` | The path does not exist.                                     |
| `413`        | `payload_too_large`     | The body is over its size limit.                             |
| `429`        | `rate_limited`          | The key is over its rate limit.                              |
| `500`        | `internal_error`        | A fault on the Whisperr side. Retry.                         |
| `502`, `504` | `upstream_unavailable`  | The Whisperr API did not answer. Retry.                      |

`POST /v1/consent/assertions` uses a shorter error body. See [Contact consent](/concepts/consent/#record-consent-proof-with-a-consent-assertion).

Every response carries an `X-Request-Id` header with the same value as `error.request_id`. To use your own id, send an `X-Request-ID` header.

## Common questions

### Can I call the Whisperr ingestion API from a browser?

Yes, for POST /v1/events/batch, POST /v1/events/track and POST /v1/identify with the publishable key (wpk_…). These three endpoints allow cross-origin requests from any origin. The other endpoints need the server key and do not answer browser preflight requests.

### Does the Whisperr API need a Content-Type header?

The ingestion endpoints read the body as JSON whatever the Content-Type is. Send Content-Type: application/json anyway, as every Whisperr SDK does.

### What happens when I send a field the Whisperr API does not know?

The request fails with 400 invalid_json. Whisperr rejects unknown fields so that a misspelled field name is never silently ignored. In a batch, only the event with the unknown field is rejected.

### How do I trace a failed request with Whisperr support?

Every response carries an X-Request-Id header, and every error body carries the same value as error.request_id. Quote it when you report a problem.

Source: https://docs.whisperr.net/api/overview/
