Server Events API

The Server Events API is Apex's server-to-server event ingest. It sits alongside the Web SDK (browser snippet) and Mobile SDK (Capacitor plugin); together they make up Apex's first-party data collection surface.

The Server Events API lets your backend send events to Apex without going through the browser snippet or a mobile SDK. Use it for:

  • Stripe / billing webhooks — fire a purchase_completed when an invoice pays, regardless of whether the user was identified in the browser.
  • CRM-side conversions — when your CRM (HubSpot, Salesforce, …) qualifies a lead as customer, send the event server-side so Apex stitches it to the original visitorId.
  • Custom Lambda / cron workflows — anything happening in your backend that's a meaningful conversion.

Endpoint

POST /api/v1/events

Authentication: an org-scoped server API key (apex_sk_…) from Settings → API Keys.

curl -X POST https://app.apex.inc/api/v1/events \
  -H "x-api-key: apex_sk_your_key_here" \
  -H "idempotency-key: $(uuidgen)" \
  -H "content-type: application/json" \
  -d '{
    "events": [
      {
        "type": "purchase_completed",
        "visitorId": "vis_abc123",
        "data": {
          "value": 99,
          "currency": "USD",
          "order_id": "order_42"
        }
      }
    ]
  }'

Response

The Server Events API always responds with { data, error, metadata } — the standard Apex API shape.

{
  "data": { "received": 1, "duplicates": 0, "outsideProduction": 0, "errors": [] },
  "metadata": {
    "workspaceKey": "prj_yourkey",
    "idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
    "batchSize": 1
  }
}

Contract rules

  • Batch cap: 100 events. Larger batches return 400 batch_too_large. Shard yourself.
  • Idempotency: send idempotency-key on every batch. Webhook retries are normal and you don't want them counted twice. See Retries and duplicates.
  • x-apex-workspace header. Only required when your org has multiple workspaces and Apex can't auto-pick one.
  • Authorization header is accepted too: Authorization: Bearer apex_sk_… is equivalent to x-api-key.

Retries and duplicates

Apex has three layers of duplicate protection. Use the first two on every webhook handler.

The idempotency-key header (per batch).

  • Scope: your workspace plus the key. The same key in two different workspaces is two different batches. The event type is not part of it.
  • How long: Apex remembers a key for at least 24 hours.
  • A repeat: the whole batch is skipped and nothing is written. The response is 200 with { "received": 0, "duplicates": <batch size>, "deduped": true }.
  • Same key, different data: still skipped. The first batch stands, and Apex does not compare the two. Give each distinct batch its own key.
  • A failed write: if a revenue write in the batch fails, the response is 500 revenue_write_failed and the key is not remembered, so your retry goes through.

eventId on each event (per event).

  • Set it to something stable from your system, such as order-123-placed. Apex counts each eventId once per workspace for at least 24 hours, even when the retry arrives under a different idempotency-key or in a different batch.
  • Repeats are skipped and counted in duplicates. The rest of the batch is written normally.
  • As with the batch key, an event whose revenue write failed is not remembered, so it can be retried.

order_id on purchases. A purchase is counted once per order_id in data. The first one with a positive amount wins. This has no time limit.

A retry more than 24 hours later is caught by order_id for purchases, but not by the other two layers. For anything else that has to be counted exactly once, retry within a day.

Staging and test backends

One server key can serve every deployment. Tell Apex which one sent the event:

  • Per deployment: set the x-apex-environment header to production, beta or dev. Put it in each deployment's config once.
  • Per event: set environment on the event. It wins over the header.

Apex also accepts staging and preview for Beta, and test, development and localhost for Dev. Anything else is rejected: a misspelled header returns 400 invalid_environment, and a misspelled event comes back in errors with invalid_environment. A typo never quietly counts as Production.

Beta and Dev events, and events with "testMode": true, are stored and show on the dashboard under their environment. They never count as revenue and are never sent to your ad platforms. The response counts them in outsideProduction.

An event with no environment shows as Unclassified on the dashboard, but it still counts as revenue and can be sent to your ad platforms. If your staging backend sends purchases, give it the header.

When a person declines tracking

Pass the person's answer from your consent banner or Apple's tracking prompt as consent on the event:

{ "type": "purchase_completed", "email": "jo@example.com", "consent": "denied", "data": { "value": 99, "currency": "USD", "order_id": "order_42" } }
  • "denied": the event still counts in your reports and revenue. Apex never sends it to an ad platform.
  • "granted": Apex may send it to the ad platforms you've connected, under your workspace's consent settings.
  • Left out: purchases and subscriptions go out, because they are your own sales. Signups and leads follow your workspace's default for events with no answer.

Any other value comes back in errors with invalid_consent.

Where the person is

Your server sends from its own location, so Apex never guesses a country for a server event. Pass the person's country as a two-letter code (ISO 3166), from the order's address or the country your edge saw when they signed up:

{ "type": "purchase_completed", "email": "jo@example.com", "country": "DE", "data": { "value": 99, "currency": "EUR", "order_id": "order_43" } }
  • If your workspace asks for consent only where the law requires it, a sale or lead from a country outside those places goes to your ad platforms, and one with no country is held back.
  • When the event belongs to someone Apex already knows, the country is kept on them, and it decides whether they can join your ad audiences.
  • A value that isn't a two-letter code ("Germany") doesn't stop the event. It counts as usual, the country is treated as unknown, and Apex lists it under data issues on your Integrations page.

What Apex does with an email

Put the email at the top of the event ("email": "jo@example.com") or in data.email. Apex uses it to find the person, so the sale lands on the right customer.

When a sale is sent to your ad platforms, the email goes with it so the platform can match the buyer. It is hashed before it leaves Apex. An event marked "consent": "denied" is never sent, so its email is only used to find the person.

How the Server Events API fits the rest of Apex

SDKWhere it runsWhat it sees
Web SDK (apex.js snippet + @apex-inc/sdk)Browser tabAnonymous visitors, identifies, custom events, web purchases
Mobile SDK (Capacitor plugin)Native iOS / AndroidApp installs, in-app purchases, device-level identifiers
Server Events API (this)Your backendServer-side conversions, webhook-driven events, CRM milestones

The same visitorId works across all three. When the Server Events API receives an event with a visitorId that the Web SDK previously created, the events show up on the same person in the Conversion Loop view.

OpenAPI spec

The machine-readable spec for the whole Apex API, this endpoint included, is at https://app.apex.inc/api/openapi?format=yaml (drop ?format=yaml for JSON). Import that link into Postman or Insomnia, or point the openapi-generator CLI at it to scaffold a client in your language.

Dashboard surface

When events arrive via the Server Events API, the Server Events health card lights up on /dashboard/settings/workspace showing recent volume, last-seen timestamp, and any errors. If the card stays grey for a workspace whose API key was issued, the typical cause is a wrong workspaceKey header or a revoked key.