API reference
The OpenAPI contract, Swagger UI, and the analytics, events, experiment and purchase routes with the permission each one requires.
The contract
The API is defined with OpenAPI and the contract is generated from the code, so it cannot describe routes that do not exist. Two URLs, both public:
- https://api.broctic.com/docs — Swagger UI, browsable.
- https://api.broctic.com/doc — the OpenAPI JSON document, for code generation.
The typed TypeScript SDK validates every response against the same @broctic/core schemas the routes are declared with, which is what keeps client and server from drifting apart.
Authentication
Organization API keys
Server-to-server calls use an organization API key, prefix brk_, created under Settings → API keys. Send it as x-api-key, or as a Bearer token through the SDK. It is scoped to one organization and carries that key's permissions.
curl https://api.broctic.com/v1/analytics?days=30 \
-H "x-api-key: brk_…"Publishable keys
The routes a shipped app calls are the exception: event ingest, the onboarding funnel it should show, and its purchases. They take a per-app publishable key, prefix bpk_, in the x-broctic-key header, and can do nothing except those things for that one app. The key is meant to ship inside an app binary. See events.
Sessions
The console authenticates with a session cookie rather than an API key. Browser code should never hold a brk_ key.
App Store analytics routes
| Route | What it does | Requires |
|---|---|---|
| GET /v1/apple/connection | The App Store Connect connection and its status | analytics: read |
| POST /v1/apple/connection | Connect an App Store Connect key | integration: manage |
| DELETE /v1/apple/connection | Disconnect and delete the key | integration: manage |
| GET /v1/apple/apps | Apps the key can see, and what they are linked to | analytics: read |
| POST /v1/apple/apps/refresh | Re-ask Apple which apps exist | integration: manage |
| POST /v1/apple/apps/link | Link an Apple app to a Broctic app | integration: manage |
| GET /v1/analytics | The read model: totals, series, breakdowns, freshness | analytics: read |
| POST /v1/analytics/sync | Pull the days that need attention now | analytics: sync |
sync sits with read rather than with integration: manage: it exposes nothing a reader cannot already see, and gating it behind admin left members watching figures they could not refresh.
Event, funnel and insight routes
| Route | What it does | Requires |
|---|---|---|
| POST /v1/events | Ingest a batch of up to 100 product events | publishable key (bpk_) |
| GET /v1/apps/{id}/keys | The app's publishable keys (prefixes only) | app: read |
| POST /v1/apps/{id}/keys | Mint a publishable key — the only time it is shown | app: update |
| DELETE /v1/apps/{id}/keys/{keyId} | Revoke a publishable key | app: update |
| GET /v1/apps/{id}/events/summary | What has arrived: names, people, platforms, versions | analytics: read |
| GET /v1/apps/{id}/funnels | The app's funnels | analytics: read |
| POST /v1/apps/{id}/funnels | Create a funnel | app: update |
| PUT /v1/apps/{id}/funnels/{funnelId} | Edit a funnel's steps | app: update |
| DELETE /v1/apps/{id}/funnels/{funnelId} | Delete a funnel | app: update |
| GET /v1/apps/{id}/funnels/{funnelId}/report | Step counts, rates, median times, verdicts | analytics: read |
| GET /v1/apps/{id}/insights | Store-side conversions plus the default funnel, with recommendations | analytics: read |
Only the first row is called by a shipped app, and it is the only one that takes a publishable key. Everything else is a console route: a signed-in member of the organization that owns the app. An app id belonging to another organization returns 404, not 403 — whether it exists is not something to confirm.
Routes a shipped app calls
| Route | What it does | Requires |
|---|---|---|
| GET /v1/experiments | The onboarding funnel this person should see, and the arm it came from | publishable key (bpk_) |
| GET /v1/purchases/entitlement | Whether this person may use what they paid for | publishable key (bpk_) |
| POST /v1/purchases/verify | Hand over a StoreKit 2 transaction the moment it completes | publishable key (bpk_) |
| POST /v1/purchases/notifications | App Store Server Notifications V2, from Apple | Apple's signature |
Each of these fails well: an app that cannot reach them keeps the funnel it shipped with and the entitlement the device already knows. The last row is called by Apple, not the app, and is authenticated by Apple's signature checked against a pinned root. See purchases.
Purchase key routes
| Route | What it does | Requires |
|---|---|---|
| GET /v1/purchases/connection | The organization's In-App Purchase key and its status — never the key itself | integration: manage |
| PUT /v1/purchases/connection | Connect an In-App Purchase key | integration: manage |
| DELETE /v1/purchases/connection | Disconnect and delete the key | integration: manage |
Limits and conventions
- Rate limits are per identity: 240 requests a minute on /v1/*, 60 a minute on log ingest, and 600 batches a minute per publishable key on event ingest — the last one is keyed by the key rather than the IP, because a fleet of phones behind one carrier NAT is a single address. The purchase routes a device calls allow 1,200 a minute per app, and Apple's notifications 600 a minute per app.
- Request bodies are capped at 64KB.
- Money is in minor units — an integer, times 100 — because floats drift and money must not.
- Dates in App Store data are Pacific dates. Timestamps elsewhere are ISO 8601 with an offset.
- Cross-organization access returns 404 rather than 403: the existence of another organization's app is not something to confirm.