Design decisions
A short record of the choices we made when designing v1, the rationale, and what we'd revisit in v2.
Resource IDs are opaque
v1 ids are Mongo ObjectId strings, but you should treat them as opaque tokens. We may switch to a different format (publicId nanoid) in v2 — keeping the type as "an opaque string" lets us migrate without breaking clients. Don't parse them, sort by them, or assume a length.
Tier access: any paid plan
The API is available on any paid tier (Basic, Standard, Plus). Rate limits scale with the plan — see Rate limits — but the surface itself is the same on all paid tiers. Free accounts get a 402 plan_required response.
We considered Plus-only access at launch. We chose any-paid because the API is the most natural way for Basic/Standard customers to integrate with their own data warehouses, and gating it would push them toward CSV exports — strictly worse for both sides.
SDKs: TypeScript + Python at launch
Generated/hand-rolled clients ship for TypeScript and Python. Other languages can use the OpenAPI spec with an OpenAPI generator of their choice — we don't plan to maintain hand-written clients for Ruby/Go/etc. unless demand makes it worth the cost.
No UE-pool recruiting via API
You can invite participants from your own pool to engage tests, but you cannot recruit from the UE-managed participant pool through the API. UE-pool recruiting requires our quality + abuse review and currently stays in-app. We'll revisit if/when the controls can be expressed as API constraints.
Bearer auth, not OAuth
API keys are long-lived shared secrets, scoped to the workspace. We don't ship per-user OAuth at launch because:
- The integrations we see in the wild (Zapier, custom scripts, BI exports) are all server-to-server with a single workspace token.
- Per-user OAuth adds significant complexity (consent flow, refresh tokens, granular scopes) that would slow v1.
Per-key scopes (read-only, project-scoped, etc.) are a planned v1.x addition.
Webhooks before streaming
We chose retry-with-backoff webhooks over server-sent events / WebSockets for v1. Webhooks compose better with serverless and queue-based handlers; SSE is harder to scale. Real-time chat streaming might land later as a separate /v1/.../stream endpoint.
What v2 will probably break
Things we'd change with hindsight; documented so you can build assuming we'll change them eventually:
- Switch resource ids to
publicIdnanoids - Tighten the
created_byshape (currently nullable for legacy rows) - Move
result.report_idandresult.thread_idinto typed top-level fields on the Job, so clients don't have to hunt insideresult - Drop the
participantsarray on Test (use/tests/:id/participantspaginated)
If your client gracefully ignores unknown fields and treats ids as opaque, the v1 → v2 migration will be straightforward.