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
projects:readprojects:writeorigins:readorigins:writepresets:readpresets:writeusage:readstudio:writewebhooks:write
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.