Developer API

The Developer API is the HTTP integration surface for managing PoQ projects and data.

The API is served at https://sapien-poq.up.railway.app/developer/v1.

The OpenAPI spec is machine-readable, and interactive docs are available in the browser.

Authentication

Every request needs a bearer token in the Authorization request header:

Authorization: Bearer poq_live_...

See API keys for key creation and lifecycle. Project and upload endpoints also require the key creator to be an org originator.

Conventions

JSON bodies and responses use camelCase keys.

Errors follow a Stripe-shaped envelope:

{
  "error": {
    "type": "invalid_request_error",
    "code": "project_not_found",
    "param": "projectId",
    "message": "Project not found."
  },
  "requestId": "0192f0a0-..."
}

requestId appears in error bodies and always in the X-Request-Id response header. Include it when reporting failures.

Pagination on GET /projects is keyset-based. Responses include { data, hasMore, nextCursor }. Pass nextCursor as ?cursor= on the next request. limit is 1–100 (default 20).

Endpoints

System

MethodPathSummary
GET/pingVerify the key is valid. Returns { "pong": true }.

Projects

MethodPathSummary
POST/projectsCreate an active project from a JSON spec and name. Returns projectId, specId, specVersion, status.
GET/projectsList projects in the key's org, newest first. Query: limit, cursor.
GET/projects/{projectId}Fetch one project.
PUT/projects/{projectId}Update name, description, and/or paused. At least one field required. Spec and status are immutable.

The spec object on create is a JSON mirror of the poq.toml spec. The project goes live immediately. The spec is compiled and locked at creation.

Validators

Manage the org's validator roster and per-project assignments. All endpoints are scoped to the API key's organization.

MethodPathSummary
GET/validatorsList the org validator roster, newest first. Query: limit, cursor. With ?email= (case-insensitive) it looks up a single validator instead — the response returns validator (and null data) rather than a page.
GET/validators/{validatorId}/projectsList a validator's non-revoked project assignments in the org.
POST/validators/{validatorId}/assignAssign a validator to one or more projects, each optionally granting a classId (+ evidenceNote). Set autoEnroll to promote an existing org member to validator first. Returns { assigned, alreadyAssigned, classesGranted }.
POST/validators/{validatorId}/unassignRevoke a validator from projectIds. Idempotent. Returns { unassigned }.

A validator resource carries userId, subject, email, evmAddress, isOriginator, isValidator, and joinedAt. Looking up an email that matches no user — or a user outside your organization — returns validator_not_found; the API never reveals whether a user exists elsewhere.

Validator classes

Classes are validator tiers declared in a project's spec ([validators].classes). Grant a class to record that a validator qualifies for a tier.

MethodPathSummary
GET/projects/{projectId}/classesList the classes declared in the project's spec (id, label, type, rewardCents, stakeWei, consensusWeight, model). Empty when the spec has no [validators] block.
POST/projects/{projectId}/validators/{validatorId}/classesGrant a class. Body: classId, evidenceNote (both required). Exclusive — any other active class the validator holds on the project is revoked first.
DELETE/projects/{projectId}/validators/{validatorId}/classes/{classId}Revoke a class grant. Idempotent.

Invites

Invite people to a project by email. An invite is fulfilled when the invitee logs in.

MethodPathSummary
POST/projects/{projectId}/invitesInvite an email. Optional classId (+ evidenceNote) pre-stages a class grant; sendEmail (default true) controls the invite email. Idempotent per (email, project).
GET/projects/{projectId}/invitesList the project's pending + cancelled invites, newest first. Query: limit, cursor. Accepted invitees appear under GET /validators, not here.
DELETE/projects/{projectId}/invites/{inviteId}Cancel a pending invite. An already-accepted invite returns invite_already_accepted.

An invite resource carries id, email, projectId, status (pending, accepted, or cancelled), assignedAt, cancelledAt, invitedByEmail, emailSentAt, createdAt, and activeClassIds.

Upload sessions

MethodPathSummary
POST/projects/{projectId}/upload-sessionsOpen a session. Declare files (relPath, size). Returns presigned PUT URLs.
POST/projects/{projectId}/upload-sessions/{uploadId}/filesAdd files or refresh URLs for an open session. Manifest is merged, never replaced.
GET/projects/{projectId}/upload-sessions/{uploadId}Session status, declared files, verification counters.
PUT/projects/{projectId}/upload-sessions/{uploadId}/fileOne-shot upload: 307 redirect to a presigned URL. Query: path (required), size (optional).
POST/projects/{projectId}/upload-sessions/{uploadId}/processVerify uploads and start the ingest run. Returns runId (202). Optional: dryRun, skipExisting.
GET/projects/{projectId}/upload-sessions/{uploadId}/statusPoll session lifecycle and the latest ingest run.

Upload workflow

File bytes never pass through the Developer API. The API issues presigned PUT URLs; the client uploads directly to object storage.

StepAction
1POST .../upload-sessions with the file manifest (relPath, size in bytes).
2PUT each file's bytes to its uploadUrl (no Authorization header on these PUTs).
3Optionally POST .../files to declare more files or refresh expired URLs.
4POST .../process to verify uploads and start ingest.
5GET .../status until the run reaches complete or failed.

Limits: 5 GiB per file; up to 1000 files per declare call; paths must be relative (no leading /, no ..). Presigned URLs expire after one hour. Request fresh ones via declare-files.

One-shot redirect: PUT .../file?path=data/rows.csv responds with 307 to the presigned URL before reading the body. Works well with curl -T:

curl -T ./data/rows.csv \
  'https://sapien-poq.up.railway.app/developer/v1/projects/{projectId}/upload-sessions/{uploadId}/file?path=data/rows.csv' \
  -H 'Authorization: Bearer poq_live_...' \
  --location

For large files or unfamiliar HTTP clients, prefer the two-step declare + PUT flow.

Datapoints

MethodPathSummary
GET/datapoints/{datapointId}/consensusFetch a datapoint's terminal consensus outcome: qualityRatingMean, qualityRatingMedian, consensusStrength, and the consensusDecisionId.

The response mirrors the datapoint.consensus_reached webhook payload. A datapoint still under validation returns consensus_not_finalized; one outside the key's org returns datapoint_not_found.

Proof Report

MethodPathSummary
GET/projects/{projectId}/proof-reportIssue and return the project's signed poq.attestation/v1 Proof Report.

The report is built and signed on demand from the project's finalized consensus data. The response carries the two headline ratings (qualityRating, consensusStrength), the attestationRoot, the signingKeyId, the canonical payload, and the compact jws. The jws verifies offline against the published JWKS with no follow-up fetch. Returns proof_report_not_ready until the dataset finishes draining, and proof_report_signing_unavailable when the deployment has no signing key configured.

Error codes

Stable codes are part of the public contract. Renaming or removing a code is a breaking change.

Authentication

CodeHTTPMessage
api_key_missing401Missing or invalid Authorization header.
api_key_malformed401Invalid API key format.
api_key_invalid401Invalid or expired API key.

Request validation

CodeHTTPMessage
parameter_invalid400Invalid request parameter.
request_body_invalid400Invalid request body.
request_body_malformed400Malformed request body.
unknown_endpoint404Unknown endpoint.

Projects

CodeHTTPMessage
api_key_actor_unavailable403This API key has no user authorized to create projects.
not_org_originator403The API key owner is not an originator for this organization.
project_not_found404Project not found.
project_already_active409Project is already active.
project_spec_invalid400The project spec is invalid. (Message carries the specific parse error.)
qualification_catalog_conflict409A qualification field conflicts with the existing catalog.

Upload sessions

CodeHTTPMessage
upload_session_not_found404Upload session not found.
upload_session_not_open409Session is no longer open for new files.
upload_session_not_ready409Cannot start a run (in flight or already complete).
upload_no_files400No files were declared.
upload_file_path_invalid400A declared path is absolute or escapes the session folder.
upload_id_required400An upload session ID is required.
upload_files_missing409Declared files not yet uploaded (message names paths).
project_spec_missing409Project has no compiled spec.

Validators and classes

CodeHTTPMessage
validator_not_found404Validator not found in this organization.
class_not_in_spec400classId is not declared in the project's validator spec.

Invites

CodeHTTPMessage
invite_not_found404Invite not found.
invite_already_accepted409Invite has already been accepted and cannot be cancelled.

Datapoints

CodeHTTPMessage
datapoint_not_found404Datapoint not found.
consensus_not_finalized409This datapoint has not reached terminal consensus yet.

Proof Report

CodeHTTPMessage
proof_report_not_ready404No finalized Proof Report yet; the project's dataset has not finished draining.
proof_report_signing_unavailable503Proof Report signing is not configured for this deployment.

Server

CodeHTTPMessage
internal_error500Internal server error.

See also

PageDescription
MCP serverSame operations as MCP tools
API keysCreation and authorization
poq.toml specAuthoring the spec field
Quickstart - APIEnd-to-end walkthrough