API reference

Base URL: https://api.picsoar.com. Everything is JSON over HTTPS.

Authentication

Authorization: Bearer ik_live_<keyId>_<secret>

Keys are bound to one project and carry explicit scopes. Create one per integration: revoking a shared key is an outage for everything that used it.

Scopes

Error envelope

{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "One or more fields are invalid.",
    "requestId": "8f2c1a5e...",
    "details": [{ "path": "host", "message": "...", "code": "..." }],
    "docs": "https://docs.picsoar.com/...",
    "retryAfterSeconds": 30
  }
}

Branch on code. Never parse message — it is written for humans and will be reworded. Never branch on the HTTP status alone; several codes share one.

Every code

Code
BAD_REQUEST
VALIDATION_FAILED
INVALID_PATH
INVALID_PRESET
UNSUPPORTED_PARAMETER
INVALID_ORIGIN
UNAUTHENTICATED
INVALID_CREDENTIALS
FORBIDDEN
INSUFFICIENT_SCOPE
NOT_FOUND
METHOD_NOT_ALLOWED
CONFLICT
IDEMPOTENCY_KEY_REUSED
ALREADY_EXISTS
QUOTA_EXCEEDED
ENTITLEMENT_REQUIRED
RATE_LIMITED
PAYLOAD_TOO_LARGE
INTERNAL
NOT_IMPLEMENTED
UPSTREAM_UNAVAILABLE
ORIGIN_ERROR
TRANSFORM_FAILED
CONFIG_UNAVAILABLE

A resource belonging to another organization returns NOT_FOUND, neverFORBIDDEN. That is deliberate: a 403 would confirm the resource exists and turn identifier enumeration into a tenant-discovery tool.

Pagination

GET /v1/projects?limit=25
{ "data": [...], "nextCursor": "prj_01H8..." }

GET /v1/projects?limit=25&cursor=prj_01H8...

Cursors are opaque — do not parse them. Offset pagination is not offered because it skips or duplicates rows when the underlying set changes between pages, which on a usage list it always does.

Idempotency

Idempotency-Key: 4f9c2b1e-…

Send it on any POST that creates something billable or externally visible. Replaying a key with the same body returns the original response. Replaying it with adifferent body is a 409 IDEMPOTENCY_KEY_REUSED rather than a silent overwrite — which is the failure mode you actually want to hear about.

Rate limits

A 429 carries retryAfterSeconds and a Retry-After header. Back off; do not retry immediately. The SDK does this for you and jitters the delay, because unjittered retries recreate the outage they were reacting to.