# POST /v1/events (/docs/api/reference/events/forward)



<Endpoint method="POST" path="/v1/events" scope="events:write" phase="1">
  Server-side event forwarding. Use when the customer can't or won't ship the client SDK — your backend posts events into the same pipeline.
</Endpoint>

This is the partner-API surface for server-side event forwarding. It's distinct from the SDK runtime path (`https://in.layers.com/events` with `X-App-Id` SDK auth) — partners authenticate with a normal Bearer key and the events route through the same downstream pipeline (storage in `sdk_events`, attribution, CAPI relay).

Use this when:

* The customer's app is a backend service or batch job — there's no client to ship the SDK to.
* You want to send a synthesized event from a webhook (e.g. Stripe `checkout.session.completed`).
* You're backfilling historical events into Layers' attribution.

For partner-issued client-side tracking, use one of the [SDK guides](/docs/sdk).

## Request [#request]

Send a batch of up to 100 events per call. Each event maps to a single row in `sdk_events`.

<Parameters
  title="Body"
  rows="[
  { name: 'projectId', type: 'string (uuid)', required: true, description: 'Project to ingest into.' },
  { name: 'sdkAppId', type: 'string', required: true, description: 'The SDK app the events are attributed to. The CAPI relay uses this to resolve Meta / TikTok configs.' },
  { name: 'events', type: 'object[]', required: true, description: '1-100 events. Each must conform to the event schema below.' },
]"
/>

Each event:

<Parameters
  title="events[]"
  rows="[
  { name: 'name', type: 'string', required: true, description: 'Canonical event name. See [Standard events](/docs/sdk/standard-events).' },
  { name: 'occurredAt', type: 'string (ISO 8601, UTC Z)', required: true, description: 'When the event occurred — set to the original timestamp, not &#x22;now&#x22;. Used for time-series rollups and CAPI dedup.' },
  { name: 'appUserId', type: 'string', description: 'Stable user handle. Used for cross-device attribution.' },
  { name: 'eventId', type: 'string', description: 'Idempotency key per event. Reusing the same `eventId` against the same `(projectId, sdkAppId)` is a no-op.' },
  { name: 'properties', type: 'object', description: 'Free-form event payload. Reserved keys: `revenue`, `currency`, `product_id`, `quantity`.' },
  { name: 'context', type: 'object', description: 'Standard SDK context fields. `{ ip?, userAgent?, locale?, timezone?, app: { version }, device: { os, osVersion, model } }`.' },
  { name: 'identity', type: 'object', description: 'PII for advanced matching. `{ email?, phone?, externalId?, fbp?, fbc?, fbLoginId? }`. All fields are SHA-256-hashed at the relay before forwarding to Meta / TikTok.' },
]"
/>

## Idempotency [#idempotency]

Two layers:

1. **Per-batch:** `Idempotency-Key` header on the request as usual.
2. **Per-event:** `eventId` field on each event. The relay dedups by `(projectId, sdkAppId, eventId)` — partners can blindly retry batches without producing duplicate platform-side events.

If you don't supply `eventId`, Layers generates one server-side from a hash of `(name, occurredAt, appUserId, properties)`.

## Example [#example]

```bash
curl -X POST https://api.layers.com/v1/events \
  -H "Authorization: Bearer $LAYERS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "projectId": "prj_254a4ce1...",
    "sdkAppId": "app_8ffb9410eb0eb848264f8a65",
    "events": [
      {
        "name": "Purchase",
        "occurredAt": "2026-05-08T14:32:11Z",
        "appUserId": "user_42",
        "eventId": "evt_stripe_pi_3KZ4M2A...",
        "properties": {
          "revenue": 19.99,
          "currency": "USD",
          "product_id": "premium_yearly"
        },
        "identity": {
          "email": "alice@example.com"
        }
      }
    ]
  }'
```

```json
{
  "accepted": 1,
  "skipped": 0,
  "failed": 0,
  "details": [
    {
      "eventId": "evt_stripe_pi_3KZ4M2A...",
      "status": "accepted"
    }
  ]
}
```

## Responses [#responses]

<Response status="202" description="Batch accepted. Events queue downstream — storage is synchronous, CAPI forwarding happens within ~1 minute.">
  ```json
  {
    "accepted": 1,
    "skipped": 0,
    "failed": 0,
    "details": [
      { "eventId": "evt_…", "status": "accepted" }
    ]
  }
  ```

  `status` per event: `accepted`, `skipped` (duplicate `eventId`), or `failed` (validation error — `details[].error` carries the cause).
</Response>

<Response status="400" description="Validation error.">
  ```json
  {
    "error": {
      "code": "VALIDATION",
      "message": "events[0].occurredAt must be ISO-8601",
      "requestId": "req_..."
    }
  }
  ```
</Response>

## Limits [#limits]

* **100 events per request.** Larger batches are rejected with `VALIDATION`.
* **`occurredAt` lookback window: 7 days.** Older events return `accepted` but are not relayed to CAPI (Meta and TikTok both refuse old events). Backfill > 7 days is stored only.
* **Per-app rate limit: 1000 events/sec sustained.** Bursts are smoothed; sustained excess returns `429 RATE_LIMITED`.

## See also [#see-also]

* [SDK health concept](/docs/api/concepts/sdk-health) — when to forward server-side vs client-side
* [`POST /v1/projects/:id/sdk-apps/:appId/verify-tracking`](/docs/api/reference/sdk-apps/verify-tracking) — active probe
* [Standard events](/docs/sdk/standard-events)
* [Custom events](/docs/sdk/custom-events)
* [Webhooks](/docs/api/operational/webhooks)
