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
| Method | Path | Summary |
|---|---|---|
GET | /ping | Verify the key is valid. Returns { "pong": true }. |
Projects
| Method | Path | Summary |
|---|---|---|
POST | /projects | Create an active project from a JSON spec and name. Returns projectId, specId, specVersion, status. |
GET | /projects | List 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.
| Method | Path | Summary |
|---|---|---|
GET | /validators | List 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}/projects | List a validator's non-revoked project assignments in the org. |
POST | /validators/{validatorId}/assign | Assign 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}/unassign | Revoke 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.
| Method | Path | Summary |
|---|---|---|
GET | /projects/{projectId}/classes | List 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}/classes | Grant 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.
| Method | Path | Summary |
|---|---|---|
POST | /projects/{projectId}/invites | Invite an email. Optional classId (+ evidenceNote) pre-stages a class grant; sendEmail (default true) controls the invite email. Idempotent per (email, project). |
GET | /projects/{projectId}/invites | List 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
| Method | Path | Summary |
|---|---|---|
POST | /projects/{projectId}/upload-sessions | Open a session. Declare files (relPath, size). Returns presigned PUT URLs. |
POST | /projects/{projectId}/upload-sessions/{uploadId}/files | Add 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}/file | One-shot upload: 307 redirect to a presigned URL. Query: path (required), size (optional). |
POST | /projects/{projectId}/upload-sessions/{uploadId}/process | Verify uploads and start the ingest run. Returns runId (202). Optional: dryRun, skipExisting. |
GET | /projects/{projectId}/upload-sessions/{uploadId}/status | Poll 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.
| Step | Action |
|---|---|
| 1 | POST .../upload-sessions with the file manifest (relPath, size in bytes). |
| 2 | PUT each file's bytes to its uploadUrl (no Authorization header on these PUTs). |
| 3 | Optionally POST .../files to declare more files or refresh expired URLs. |
| 4 | POST .../process to verify uploads and start ingest. |
| 5 | GET .../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
| Method | Path | Summary |
|---|---|---|
GET | /datapoints/{datapointId}/consensus | Fetch 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.
PoQ Report
| Method | Path | Summary |
|---|---|---|
GET | /projects/{projectId}/proof-report | Issue and return the project's signed poq.attestation/v1 PoQ 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
| Code | HTTP | Message |
|---|---|---|
api_key_missing | 401 | Missing or invalid Authorization header. |
api_key_malformed | 401 | Invalid API key format. |
api_key_invalid | 401 | Invalid or expired API key. |
Request validation
| Code | HTTP | Message |
|---|---|---|
parameter_invalid | 400 | Invalid request parameter. |
request_body_invalid | 400 | Invalid request body. |
request_body_malformed | 400 | Malformed request body. |
unknown_endpoint | 404 | Unknown endpoint. |
Projects
| Code | HTTP | Message |
|---|---|---|
api_key_actor_unavailable | 403 | This API key has no user authorized to create projects. |
not_org_originator | 403 | The API key owner is not an originator for this organization. |
project_not_found | 404 | Project not found. |
project_already_active | 409 | Project is already active. |
project_spec_invalid | 400 | The project spec is invalid. (Message carries the specific parse error.) |
qualification_catalog_conflict | 409 | A qualification field conflicts with the existing catalog. |
Upload sessions
| Code | HTTP | Message |
|---|---|---|
upload_session_not_found | 404 | Upload session not found. |
upload_session_not_open | 409 | Session is no longer open for new files. |
upload_session_not_ready | 409 | Cannot start a run (in flight or already complete). |
upload_no_files | 400 | No files were declared. |
upload_file_path_invalid | 400 | A declared path is absolute or escapes the session folder. |
upload_id_required | 400 | An upload session ID is required. |
upload_files_missing | 409 | Declared files not yet uploaded (message names paths). |
project_spec_missing | 409 | Project has no compiled spec. |
Validators and classes
| Code | HTTP | Message |
|---|---|---|
validator_not_found | 404 | Validator not found in this organization. |
class_not_in_spec | 400 | classId is not declared in the project's validator spec. |
Invites
| Code | HTTP | Message |
|---|---|---|
invite_not_found | 404 | Invite not found. |
invite_already_accepted | 409 | Invite has already been accepted and cannot be cancelled. |
Datapoints
| Code | HTTP | Message |
|---|---|---|
datapoint_not_found | 404 | Datapoint not found. |
consensus_not_finalized | 409 | This datapoint has not reached terminal consensus yet. |
PoQ Report
| Code | HTTP | Message |
|---|---|---|
proof_report_not_ready | 404 | No finalized PoQ Report yet; the project's dataset has not finished draining. |
proof_report_signing_unavailable | 503 | PoQ Report signing is not configured for this deployment. |
Server
| Code | HTTP | Message |
|---|---|---|
internal_error | 500 | Internal server error. |
See also
| Page | Description |
|---|---|
| MCP server | Same operations as MCP tools |
| API keys | Creation and authorization |
| poq.toml spec | Authoring the spec field |
| Quickstart | End-to-end walkthrough |