OpenStudio API

The REST API for building on top of the OpenStudio platform — programmatic access to your organization's data. Every request is authenticated with an API key, and every response follows the same predictable shape.

New endpoints are added gradually, based on what our tenants need — the ones below are today's set, not the full picture. If your integration needs something specific, just open a ticket with our developers team.

Base URL: https://api.openstudio.one/v1 — no trailing slash. Endpoints are appended with their own leading slash, e.g. /v1 + /ping = /v1/ping.

Prefer Postman or Insomnia? Import /v1/openapi.json directly — both tools read OpenAPI specs natively. Download OpenAPI Spec

Reading this reference

Field descriptions below use four different styles of inline code so you can tell what kind of thing is being pointed at without reading the whole sentence:

StyleMeaning
field_nameA field or parameter name — something you'd read from a response or send in a request, e.g. queue_id or {id} in a URL.
trueA literal value a field can hold — a boolean, an enum member.
422An HTTP status code — what the response itself comes back as, not a value stored in any field.
PATCH /tickets/{id}A reference to another endpoint or HTTP method, colored the same as that method's tag in the sidebar on the left. If it names a real endpoint on this API, it's a clickable link straight to that section.

A field name in a Field/Response table can also carry a small icon right after it, instead of the same thing being said in prose every time:

IconMeaning
This field can be null. Appears in its own "Flags" column on request/response tables — hover (or tab to) the icon for the tooltip.
Deprecated — avoid using this in new integrations. Same column, same hover behavior.

Authentication

Two keys exist: a tenant key, tied to your organization, and a personal key, tied to an individual user. Which one an endpoint needs depends on what it does:

HeaderWhen it's needed
X-Api-Tenant-KeyOrganization-level settings and actions
X-Api-User-KeyActions performed as a specific person
Both headersAdmin-level actions that need to know both who's acting and on whose behalf

Where to find your keys

KeyWhere to find it
Tenant keyOrganization admins: Administration Control Panel General General settings API settings
Personal keyAny user, for their own key: Sidebar Data Manage account API access

Both pages show the key masked by default (click the eye icon to reveal it — it's never a one-time reveal, you can always look at it again later) plus a live view of your rate limit, recent usage, and recent activity on that key.

What you can do with a key

From either page, the key's owner (a tenant admin for the tenant key; a user for their own personal key) can, at any time and without contacting support:

ActionEffect
RegenerateCreates a brand-new key. Defaults to a 48-hour grace period — see below — with an "immediate cutover" option if you need the old key dead right away (e.g. it may be compromised).
SuspendImmediately blocks every request using that key, without deleting it. Self-suspended keys can be self-reactivated at any time — see the staff note below for the one case where that's NOT true.
ReactivateRe-enables a self-suspended key. Only works if you suspended it yourself.
Set to read-onlyRestricts the key to GET requests only — see below.

Every one of these requires you to confirm first, and every confirmation explains exactly what will happen — none of them are one-click actions.

Read-only keys

Either key can be restricted to read-only — GET requests only, everything else rejected with read_only_key. Useful for an integration that should never be able to modify data, even if the key leaks. Toggle this from the key's own management page.

Grace period on regenerate

Regenerating a key defaults to a grace period — the old key keeps working alongside the new one for 48 hours, so a live integration has time to switch over instead of breaking the instant you regenerate. An immediate hard cutover is also available if you need one. Any request that authenticates using a key still in its grace period gets X-API-Key-Deprecated: true and X-API-Key-Grace-Period-Ends response headers — check for these so your integration can prompt you to update before the old key actually stops working.

What OpenStudio staff can do

OpenStudio staff can also regenerate or suspend either key — for example as part of an incident response, or at your request via a support ticket (see "Getting help" below). A key suspended by staff cannot be reactivated by the tenant admin or user themselves — self-service reactivation is blocked with a clear error telling you to contact support, and only staff can undo a staff-initiated suspension. This is deliberate: if OpenStudio suspends a key for a security reason, that decision shouldn't be reversible from the same account that might be compromised.

Identifying users

Two different id spaces show up around users in this API, and it's worth being explicit about which one is which:

  • user_id — this is the one that matters. Every {id} path parameter for a user, and every *_user_id field (author_user_id, operator_user_id, leader_user_id, and so on), always refers to this same identifier. There are no exceptions anywhere in this API.
  • personal_code — a secondary numeric identifier from a separate internal HR record, shown alongside user_id in a few responses (e.g. GET /users) purely for reference — it also underlies the login sso_username you may recognize from the dashboard. It is never accepted as a path parameter or a *_user_id field anywhere. If you're holding a personal_code and need the matching user_id, look it up via GET /users?q_personal_code=....

Errors & response envelope

Every response — success or failure — also carries an X-Request-ID header, a unique ID generated the moment the request was received. If you need to report an issue, include this ID — it lets us find the exact request in our logs instantly instead of searching by timestamp.

Every response, success or failure, has the same shape:

{
  "success": true,
  "data": { ... },
  "error": null
}

On failure, data is null and error carries a machine-readable code and a human-readable message:

{
  "success": false,
  "data": null,
  "error": { "code": "key_suspended", "message": "The tenant API key has been suspended." }
}

When a write request (POST/PUT) fails validation on more than one field at once, error.code is "validation_failed" and an extra details object lists every field that was wrong, so a caller who submitted 3 bad fields sees all 3 in one response instead of fixing them one at a time:

{
  "success": false,
  "data": null,
  "error": {
    "code": "validation_failed",
    "message": "One or more fields are invalid.",
    "details": {
      "brand_email": "The email provided is not a valid format.",
      "name": "The organization name is required."
    }
  }
}

details is only present for multi-field validation errors — a single-cause failure (a missing resource, a suspended key, and so on) never includes it.

Common error.code values across the API:

CodeMeaning
unauthorizedMissing or invalid API key for what this endpoint requires (401)
key_suspendedThe key itself was suspended, by its owner or by staff (403)
account_not_verifiedThe tenant account is not in "Verified" standing — pending, under warning, banned, or closed (403)
account_not_activeThe individual user account is not "Active" — pending, inactive, reported, or closed (403)
api_disabledAPI access is turned off for this tenant entirely (403)
ip_not_allowedThe request's IP address doesn't match an allow rule / matches a deny rule (403)
read_only_keyThis key is restricted to GET requests — see Authentication (403)
rate_limitedToo many requests in the current minute — see Rate limiting (429)
endpoint_disabledThis specific endpoint is temporarily disabled by staff (503)
method_not_allowedWrong HTTP verb for this route (405)
validation_failedOne or more request-body fields failed validation — see details above (422)
invalid_bodyRequest body isn't valid JSON at all (400)
invalid_parameterA query parameter has an invalid value (400)
no_fieldsA write request provided none of the fields it recognizes (400)
payload_too_largeRequest body exceeds the maximum allowed size, 5 MB (413)
not_foundThe requested resource doesn't exist (404)
internal_error / database_errorSomething failed on our side — safe to retry (500)

Pagination

Any endpoint that returns a list of resources is paginated — pulling an unbounded number of rows into one response doesn't scale, on shared hosting or otherwise. Endpoints marked Paginated accept two optional query parameters:

ParameterMeaning
pagePage number, starting at 1. Defaults to 1.
limitRows per page. Defaults to 50; capped at 200 regardless of what's requested.

The response carries a sibling pagination object alongside data, including ready-to-call URLs for the adjacent pages — same filters as the current request, just page swapped — so a client can page through results without reconstructing the request itself:

{
  "success": true,
  "data": { "...": "..." },
  "error": null,
  "pagination": {
    "page": 1,
    "limit": 50,
    "total": 134,
    "total_pages": 3,
    "next_page_url": "https://api.openstudio.one/v1/departments?page=2&limit=50",
    "prev_page_url": null
  }
}

next_page_url is null on the last page, prev_page_url is null on the first — check for null rather than comparing page to total_pages yourself.

Filtering & sorting

Endpoints marked Paginated use the same query-string shape for filtering and sorting, so you only need to learn it once. Each endpoint's own reference lists exactly which fields it allows — sending a field it doesn't recognize returns a 422 invalid_parameter naming the allowed ones.

Sorting

ExampleMeaning
?sort=titleAscending by title
?sort=-created_atDescending by created_at — a leading - reverses the direction
?sort=title,-created_atMultiple fields, applied left to right

Filtering

Bracket notation, one bracket pair per field:

ExampleMeaning
?filter[level]=2Exact match — level equals 2
?filter[created_at][gte]=2026-01-01Greater-than-or-equal
?filter[created_at][lte]=2026-12-31Less-than-or-equal — combine with gte for a range
?filter[title][like]=SalesSubstring match, case-insensitive

Combining multiple filters and multiple sort fields

Filter on as many fields as you like at once — just repeat filter[...] for each one, joined with & like any other query params. Every filter is AND'ed together (a row must match ALL of them, not any one):

GET /v1/departments?filter[level]=2&filter[title][like]=Sales

Sorting by multiple fields works the same way, but inside the single sort parameter — comma-separated, applied left to right as tie-breakers (the first field sorts first; where two rows are equal on it, the second field decides their order):

GET /v1/departments?sort=title,-created_at

And all of it combines into one request — filters, multi-field sort, and pagination together:

GET /v1/departments?filter[level]=2&filter[title][like]=Sales&sort=title,-created_at&page=2&limit=25

Filtering and sorting apply before pagination, so total/total_pages in the response reflect the filtered set, not the whole table.

Rate limiting

Every key has a requests-per-minute budget. By default it's shared across the whole tenant — the tenant key and every user key with no personal override all draw from the same pool. A tenant can also set a personal limit for an individual user, which checks that person's own request count instead, independent of the shared pool.

Every response includes your current standing:

HeaderMeaning
X-RateLimit-LimitThe budget that applies to this request — requests per minute.
X-RateLimit-RemainingHow many more requests you can make in the current minute.

Exceeding the limit returns a 429 with a Retry-After: 60 header:

{
  "success": false,
  "data": null,
  "error": { "code": "rate_limited", "message": "Rate limit exceeded (60 requests/minute). Try again shortly." }
}

Limits are configured per tenant (and optionally per user) by OpenStudio staff — contact your account contact if your integration needs a higher budget.

Webhooks

Set a webhook URL (ask OpenStudio staff to configure this for your tenant) and OpenStudio pushes a JSON payload to it whenever something relevant changes, instead of you having to poll for updates. Today that's just PATCH /tenant/settings — more write endpoints will add their own events as they're built.

{
  "event": "tenant.settings.updated",
  "brand_id": 42,
  "timestamp": "2026-01-15T10:32:00+00:00",
  "data": { "website": "https://example.com" }
}

Verifying a webhook came from OpenStudio

Every delivery carries an HMAC-SHA256 signature of the raw request body, computed with your tenant's webhook secret:

HeaderMeaning
X-OpenStudio-EventSame value as the event field in the body, e.g. tenant.settings.updated.
X-OpenStudio-Event-IDA UUID identifying this specific event — the same value on every retry of it. See "Retries & idempotency" below.
X-OpenStudio-Signaturesha256=<hex digest> — recompute this yourself and compare before trusting the payload.
$payload = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $payload, $your_webhook_secret);
$received = $_SERVER['HTTP_X_OPENSTUDIO_SIGNATURE'] ?? '';

if (!hash_equals($expected, $received)) {
    http_response_code(401);
    exit('Invalid signature.');
}

Always compare signatures with a constant-time function (hash_equals in PHP, or your language's equivalent) rather than ==, to avoid leaking the correct value through response-timing differences.

Retries & idempotency

If your endpoint doesn't respond with a 2xx status (or doesn't respond at all within 5 seconds), OpenStudio retries the SAME delivery — byte-identical body and signature — up to twice more: once after 1 hour, once after 24 hours. If all three attempts fail, the event is given up on (no further retries).

A retry carries the exact same X-OpenStudio-Event-ID as the original attempt. If your endpoint already processed that event ID successfully, you can safely detect the duplicate and return 2xx immediately without reprocessing — useful if your first response actually succeeded but the confirmation itself got lost in transit.

Known issues & limitations

There are no known bugs at this time. If you run into unexpected behavior, let us know and we'll investigate. A few things below aren't bugs — they're current, deliberate limitations worth knowing about before you design an integration around them.

  • Each user (and each tenant) has exactly one API key at a time. It cannot be scoped down to specific resources or actions — a key simply lets you act with whatever permissions its owner already has in the dashboard, the same as if they'd logged in themselves. The one exception is the coarse full/read_only toggle available on both self-service key-management pages, which blocks all non-GET requests when set to read_only.

Getting help

Something not behaving as documented, or need a limit raised? Reach out — which path depends on whether you already have an OpenStudio account.

I have an OpenStudio account

Open a ticket under "API Support & Integrations". By default only founders and system administrators can open tickets this way — ask one of them if you don't see the option.

Open a ticket →
I don't have an account

For example, you're integrating on a tenant's behalf. Select "API Support" as the request type — no login needed.

Submit a ticket →

Either way, include your X-Request-ID (see Errors & response envelope) if you're reporting a specific failed request — it lets us find the exact request in our logs instantly instead of reconstructing it from a description.

Browse endpoints

Every endpoint lives on one of the pages below, grouped by resource — the guide sections above stay here on the overview page no matter which one you're on.

ESC