Developer API - REST
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 specification. The project goes live immediately. The spec is compiled and locked at creation.
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.
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. |
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 specification | Authoring the spec field |
| Quickstart - API | End-to-end walkthrough |