# For the brave. API reference

Source: https://www.sheepit.ai/docs/api

No exercises and no congratulations at the end — just the endpoints, in order, with examples that actually run.

The API is hosted at `api.sheepit.ai` — every published SDK defaults to it.

- Base URL: `https://api.sheepit.ai`
- Auth header: `Authorization: Bearer lp_…`
- Success envelope: `{ "data": { … } }`
- Error envelope: `{ "error": { "code", "message" } }`
- Rate limit: `60–5,000 req/min per project, by plan (free: 60/min)`
- Content type: `application/json`

## API key types

Sheepit uses bearer tokens. Every key is prefixed by what it can do, so you can tell at a glance whether you've checked the right one into the right place.

- `lp_pub_…` **Publishable**: Client-side SDK key. Safe to ship in browser and mobile bundles. Can read /v1/config and post events. Cannot touch admin routes.
- `lp_sec_…` **Secret**: Server-side SDK key with full project access. Used by server SDKs. Never embed client-side.
- `lp_dev_…` **Dev / CLI**: Read-only access to schemas and definitions. Powers @sheepit-ai/cli codegen. Safe for dev shell + CI secrets, not client bundles.

Send the key as `Authorization: Bearer <key>`. Every request that needs auth needs this header.

## Send one event

Paste this into a terminal. Replace the key. If the response is `{ "data": { "accepted": 1 } }`, you're done — the event is in the dashboard.

```bash
curl -X POST https://api.sheepit.ai/v1/ingest \
  -H "Authorization: Bearer lp_pub_xxx_…" \
  -H "Content-Type: application/json" \
  -d '{
      "batch": [
          {
              "type": "track",
              "event": "signup",
              "properties": {
                  "source": "docs"
              }
          }
      ],
      "context": {
          "user": {
              "anonymous_id": "anon-abc"
          }
      },
      "sent_at": "2026-05-26T18:00:00Z"
  }'
```

That's the whole loop. Everything else in this page is a variation on the same shape.

## Endpoints

The endpoints the SDKs call today. New ones land here as we ship them.

- `POST /v1/devices/register` (publishable key): Register an anonymous device on app boot. Returns a device ID you store locally.
- `POST /v1/devices/:deviceId/identify` (publishable key): Attach a user ID to a previously-registered device.
- `GET /v1/config` (publishable key): Snapshot of all flags + experiment assignments for the current device / user. Cache it; refresh on app foreground.
- `POST /v1/ingest` (publishable key): Send one or many events in a single batch. Fire-and-forget; client SDKs queue and retry.
- `POST /v1/crashes/report` (publishable key): Submit a crash report. Used by the mobile SDKs.
- `POST /v1/performance/ingest` (publishable key): Batched performance metrics (startup time, frame drops, ANR, network spans).

## What can go wrong

All errors come back as `{ "error": { "code", "message" } }`. The code is stable; the message is human-readable.

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | Body didn't parse, or required fields were missing. |
| 401 | `UNAUTHORIZED` | Missing or malformed Authorization header. |
| 403 | `FORBIDDEN` | Key is real but doesn't have permission for this route (e.g. pub key on admin route). |
| 404 | `NOT_FOUND` | Resource doesn't exist or isn't visible to your project. |
| 409 | `CONFLICT` | A resource you tried to create already exists (e.g. a project slug that's taken). Duplicate events aren't a conflict — a duplicate event_id is simply deduped, not rejected. |
| 429 | `RATE_LIMIT_EXCEEDED` | You're sending too fast for your project's plan. Back off and retry with jitter. |
| 429 | `INGEST_RATE_LIMITED` | A separate per-minute cap on events sent to /v1/ingest (the rate limit above counts requests) — protects against one oversized batch draining the whole day's budget. |
| 5xx | `INTERNAL_SERVER_ERROR` | Our fault. SDKs already retry; if it persists, email us. |
