API

HTTP basics

Authentication, organization scope, responses, errors, rate limits, and streams.

REST requests use your installation's URL plus /api. Authenticate scripts with a personal access token. Create a token or use the CLI login guide.

curl "$OPENSHIP_URL/api/projects" \
  -H "Authorization: Bearer $OPENSHIP_TOKEN"

Use Content-Type: application/json for JSON bodies. Send only the fields you are changing in a partial update. The resource pages document request fields and the equivalent SDK calls.

Authentication

A request proves who it is in exactly one of four ways. Openship checks for a Bearer token first, then a session cookie, then the loopback fallback (only if that mode is enabled).

MethodWhat you sendWho uses it
Personal access tokenAuthorization: Bearer opsh_pat_…CLI, scripts, server-to-server
Session cookiehttpOnly cookie set at loginThe dashboard, in a browser
MCP OAuthAn OAuth 2.1 access token bound at consentAI agents connecting to /api/mcp
Zero-auth loopbackNothing (request from 127.0.0.1)Desktop app / opt-in single-user instance
curl https://your-host/api/projects \
  -H "Authorization: Bearer $OPENSHIP_TOKEN"

Bearer is for non-browser clients only

A Bearer token presented from a browser-trusted origin is rejected (BEARER_NOT_ALLOWED_FROM_BROWSER), so an exfiltrated session token can't be replayed past the httpOnly cookie. A token can be org-scoped (rejects a mismatched X-Organization-Id with TOKEN_ORG_SCOPE) and read-only (rejects mutations with TOKEN_READ_ONLY). MCP OAuth discovery is served at the origin root: /.well-known/oauth-authorization-server and /.well-known/oauth-protected-resource.

Permissions

Authenticated application routes declare a permission tag of the form resource:actionproject:read, domain:write, deployment:admin — and sub-resources add a middle segment (project:service:write). Shared operations also enforce access for native SDK callers. The action usually lines up with the HTTP method: read (GET one), write (POST/PUT/PATCH), admin (DELETE and destructive), list (GET a collection).

Access is decided per organization. Openship resolves which org you mean from the X-Organization-Id header (falling back to your session default), then checks your role there — owner, admin, member, or restricted (grant-only). The operation tables list the declared route permission. Shared operations also check resource ownership, current membership, token restrictions, and any additional administrative requirements. See Permissions & roles for the full model.

Rate limits

The whole /api surface is rate-limited. Unauthenticated requests fall under a per-IP default; authenticated requests get a more generous per-user budget. Some route groups carry a tighter or looser named policy. Limits are per rolling minute.

PolicyLimitKeyed byApplies to
default-anon300 / minIPAny unauthenticated route
default-authed3000 / minuserAny authenticated route
auth-tight10 / minIPPOST /api/auth/* (login, signup, reset) and self-host invite signup
mcp300 / minIP/api/mcp (tool-call bursts)
webhook-ingress120 / minsource IPInbound webhook deliveries
billing-portal20 / minorgStripe portal / checkout creation

Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset. /api/health is never limited (load balancers and SSR poll it). On a self-hosted instance with neither TRUST_PROXY nor OPENSHIP_PUBLIC_URL set, requests arriving over loopback skip the ordinary limits — the auth-tight login gate always enforces.

429 — Too many requests

When a bucket is exhausted the request gets a 429 with body {"error":"Too many requests"} plus a Retry-After header (seconds). Back off until the window resets rather than retrying immediately.

Error shape

Errors come back as JSON with a stable shape: a human error message and, for typed failures, a machine code. Validation failures add a field-level details map.

{ "error": "This access token is read-only", "code": "TOKEN_READ_ONLY" }
StatusTypical codeMeaning
400VALIDATION_ERRORRequest body failed the schema — see details for the offending fields.
401INVALID_TOKEN, BEARER_NOT_ALLOWED_FROM_BROWSERNot authenticated, or a bad/expired token (plain Unauthorized when no session at all).
403TOKEN_ORG_SCOPE, TOKEN_READ_ONLYAuthenticated but not allowed — wrong org, read-only token, or your role/grants deny the action.
404Resource doesn't exist, or isn't in your organization (IDOR-safe).
409Conflict with current state (e.g. a delete already in progress).
429Rate-limited (see above).
503AUTH_UNAVAILABLEThe auth backend is unreachable — retry; never treated as "no session".
500Unhandled server error ({"error":"Internal server error"}, no code).

Fixed organization scope

Send both X-Organization-Id: org_123 and X-Openship-Scope: fixed to require that exact organization. Authentication and grants still apply. A token bound to a different organization is rejected. The remote SDK sends these headers when configured with organizationId; it first checks API capability support. See scope compatibility.

Streams and retries

Endpoints marked as streams return Server-Sent Events, not a single JSON object. Use curl -N or an SSE client, and close the connection when done. SDK stream methods return async iterables and accept an abort signal. Deployment progress shows both interfaces.

A dropped connection does not prove a mutation failed. Inspect the resource or run status before retrying a create, deploy, or restore. The SDK does not automatically retry mutations.

Availability

Self-hosted routes are not mounted on the Cloud API. A named SDK method can still require a configured provider, host capability, or operator authority. HTTP-only rows have no named native operation; a remote client can use its HTTP transport where authentication permits it.

On this page