Skip to main content

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:

  1. The integrations we see in the wild (Zapier, custom scripts, BI exports) are all server-to-server with a single workspace token.
  2. 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 publicId nanoids
  • Tighten the created_by shape (currently nullable for legacy rows)
  • Move result.report_id and result.thread_id into typed top-level fields on the Job, so clients don't have to hunt inside result
  • Drop the participants array on Test (use /tests/:id/participants paginated)

If your client gracefully ignores unknown fields and treats ids as opaque, the v1 → v2 migration will be straightforward.