Build on Shruwd

API overview

Base URL, authentication, scopes, rate limits and the error shape shared by every Shruwd API endpoint.

Everything the dashboard does, the API does — it is the same surface, not a mirror of one.

text
https://shruwd.io/api/v1

Paths in these docs are relative to that base, as in the OpenAPI document: POST /brands is POST https://shruwd.io/api/v1/brands.

JSON in, JSON out, UTF-8. The contract describes itself in OpenAPI 3.1 at /openapi.json, which needs no key to read. The same document is also served at /api/v1/openapi.json.

Authenticating

Mint a key in the dashboard under Account → API keys, then send it as a bearer token.

curl "https://shruwd.io/api/v1/workspace" \
  -H "Authorization: Bearer $SHRUWD_API_KEY"

Keys can only be created from a signed-in session. A key uses the product; it can never administer the account — it cannot mint other keys, change billing, or delete anything that is not brand configuration.

Scopes

ScopeGrants
readEvery GET
writeBrand, prompt and entity writes; finding transitions; manual cycles; ingest tokens
defaultBoth

A request outside its key's scopes is 403 insufficient_scope.

Roles

A request acts with your role in the workspace.

RoleCan
viewerEvery read
editorPrompts, competitors, finding transitions, brand name and timezone, run a cycle
adminThe above, plus invites, connections and ingest tokens
ownerThe above, plus creating and archiving brands, and billing

A key never exceeds its holder's role. Refusals are 403 not_a_member and 403 insufficient_role, the latter carrying details: { required, role }.

Workspaces are implicit

Your key determines the workspace. No path or body ever carries a workspace id.

If you own no workspace, your first POST /brands creates one. There is no workspace resource to set up first.

A brand or finding id belonging to another workspace returns 404, not 403 — indistinguishable from an id that does not exist.

Identifiers

Ids are ULIDs and opaque. Two human-readable handles sit beside them, and both are accepted wherever the id is:

HandleUsed byExample
A brand's slug{brandId}/brands/waitlister
A finding's number{findingId}/findings/7

Finding numbers start at 1 per workspace, in first-seen order, and are never reused.

Every metric carries its uncertainty

There is no shape in this API that returns a bare number.

json
{ "state": "ok", "point": 0.142, "lo": 0.081, "hi": 0.226, "n": 42 }

point, lo and hi are proportions from 0 to 1: 0.142 is 14.2%.

state is ok, insufficient_data (fewer than ten responses — not zero), or undefined. See Reading your results.

Errors

The response body is the error. There is no wrapper.

json
{ "code": "plan_limit", "message": "…", "retryable": false, "details": { "limit": 25, "current": 25 } }
Field
codeStable and machine-readable. Branch on this, never on message
messageHuman-readable. May change at any time
retryableWhether retrying the identical request could succeed
detailsPresent when there is something specific to say

Common codes

CodeStatusMeaning
not_found404No such resource, or it belongs to another workspace
insufficient_scope403The key lacks read or write
not_a_member · insufficient_role403Your role is too low
not_entitled403Your plan does not include this
plan_limit409A plan cap is used up. details carries the numbers
rate_limited429See below. retryable: true
invalid_domain · invalid_name · invalid_timezone422Bad brand input
duplicate_domain409Another active brand here has that domain
duplicate_prompt409That prompt is already active on the brand
context_terms_required422A short or common-word competitor name needs context terms
domain_taken409Another entity of this brand owns that domain
self_entity409The brand's own entity cannot be removed
snapshot_taken409A free-plan domain has already had its one measurement
workspace_on_hold409Measurement is paused pending review
suggestion_decided409That suggestion was already accepted or dismissed
empty_patch400A PATCH with no fields

Rate limits

Per key, in fixed one-minute windows.

Per minute
GET120
Everything else30

Over the limit is 429 rate_limited with Retry-After in seconds. Honour it — the SDK does this for you.

Times and ranges

Timestamps are ISO 8601 UTC. Days are YYYY-MM-DD.

Date ranges default to today in the brand's timezone, which is the calendar the measurements are bucketed on. UTC today can be a day ahead of it.

from is clamped to your plan's history window. When that happens the response says so in historyFrom rather than failing.

Versioning

The version is in the path. Changes within v1 are additive only; anything breaking would be /api/v2.

Next steps