Facts reviewed: 2026-10-20

Create an ingestion key in your app's **API Keys** page. It needs `events:write`. Keep the secret in your server's environment or secret store; browser installations use the public Tracking ID instead.

## Send one event

Set `OWLEYE_INGEST_KEY` securely in your environment, then replace `owl_your_tracking_id` below with the matching app ID:

```sh
curl --fail-with-body 'https://api.owleye.dev/v1/events/api' \
  --header "Authorization: Bearer $OWLEYE_INGEST_KEY" \
  --header 'Content-Type: application/json' \
  --data '{
    "site_id": "owl_your_tracking_id",
    "event": {
      "type": "external",
      "name": "report_generated",
      "page": { "path": "/reports" },
      "data": { "records": { "format": "pdf", "scheduled": true } }
    }
  }'
```

`timestamp` is optional; omission uses the server's current time. An explicit timestamp must be RFC 3339, no more than 90 days old or five minutes into the future. Page and environment fields are optional. Use primitive custom fields and keep personal identifiers out of the payload.

Server-originated events are not automatically joined to a visitor in their browser. Do not forge forwarded-address headers to try to assign visitor identity.

## Send a batch

Use `events` instead of `event`, with 1–100 events belonging to the same app:

```json
{
  "site_id": "owl_your_tracking_id",
  "events": [
    {
      "type": "external",
      "name": "report_generated",
      "data": { "records": { "format": "pdf" } }
    },
    {
      "type": "external",
      "name": "report_delivered",
      "data": { "records": { "channel": "email" } }
    }
  ]
}
```

The body limit is 1 MiB. An event name is 1–128 UTF-8 bytes, and each event's data is bounded to 16 KiB. Custom event records allow up to ten primitive fields. The batch is validated before quota is reserved.

## Read the result

HTTP `202` does not always mean every event was stored. Inspect `accepted`, `status`, `event_ids`, and `dropped` when present:

| Status | Meaning |
| --- | --- |
| `accepted` | All submitted events accepted |
| `partially_accepted_monthly_limit` | Only the remaining monthly allowance was accepted |
| `monthly_event_limit_reached` | Zero accepted because the allowance is exhausted |

DNT and GPC headers do not change ingestion behavior. Apply the appropriate collection choices in the SDK or server-side sender before submitting events.

The API can also reject a paused app, a key for another app, malformed fields, or excessive traffic. Honor `Retry-After` on 429. This endpoint has no client-provided idempotency key: blindly replaying a request after a network timeout can duplicate events.