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/usage

Errors

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.

Full reference →