API overview
Everything the dashboard does, the API does. It is the same service — there is no privileged internal path.
Authentication
curl https://api.picsoar.com/v1/projects \
-H "Authorization: Bearer ik_live_01H8XKQ2M7N4P5R6S7T8V9W0XY_..."Keys are scoped and bound to one project. Create them per integration rather than sharing one: revoking a shared key means an outage for everything that used it.
Resources
GET /v1/projects
POST /v1/projects
GET /v1/projects/:id
PATCH /v1/projects/:id
DELETE /v1/projects/:id
POST /v1/projects/:id/purge
GET /v1/projects/:id/environments
GET /v1/projects/:id/origins
POST /v1/projects/:id/origins
POST /v1/projects/:id/origins/:originId/verify
DELETE /v1/projects/:id/origins/:originId
GET /v1/projects/:id/presets
PUT /v1/projects/:id/presets/:name
POST /v1/projects/:id/presets/:name/rollback
GET /v1/projects/:id/api-keys
POST /v1/projects/:id/api-keys
DELETE /v1/projects/:id/api-keys/:keyId
GET /v1/projects/:id/usageErrors
Every error, on every endpoint, has the same shape:
{
"error": {
"code": "QUOTA_EXCEEDED",
"message": "Your plan includes 1 project(s).",
"requestId": "8f2c1a5e...",
"details": [{ "path": "host", "message": "..." }],
"docs": "https://picsoar.com/pricing",
"retryAfterSeconds": 60
}
}Branch on code, not on the HTTP status and never on message. TherequestId also appears in the X-Request-Id response header; quote it in support requests and we can find the exact call.
A resource in another organization returns NOT_FOUND, notFORBIDDEN. That is deliberate — a 403 would confirm the resource exists.
Pagination and idempotency
Lists are cursor-paginated: pass ?cursor= from the previous response'snextCursor. Offsets are not offered because they skip or duplicate rows when the underlying set changes between pages.
Send an Idempotency-Key header on any POST that creates something billable or externally visible. Replaying the same key with the same body returns the original response; replaying it with a different body is a 409 rather than a silent overwrite.
SDK
import { PicSoarClient } from '@picsoar/sdk/server';
const client = new PicSoarClient({ apiKey: process.env.PICSOAR_API_KEY });
const projects = await client.listProjects();The SDK retries only idempotent requests. A bare POST is never retried — that is how duplicate resources and duplicate charges happen.