Skip to content

REST API

Projects, layouts, the catalog, quotes, webhooks, embeds, and arrangement runs — described by OpenAPI 3.1 documents with hosted reference pages. The base URL is https://api.sunnysdk.com.

Use this guide for authentication, conventions, and choosing an API surface. The Scalar references provide each endpoint’s parameters, request and response schemas, required scopes, and copyable request examples. You can also send requests from the reference using your own credential; those requests run against the selected API server and can change workspace data. Use a sandbox workspace when trying writes.

URL What
https://api.sunnysdk.com/docs Browsable reference for the unversioned API: every operation, schema, and scope.
https://api.sunnysdk.com/openapi.json Its OpenAPI 3.1 document — feed it to a client generator or API tool.
https://api.sunnysdk.com/v1/docs Browsable reference for the /v1 surface (beta).
https://api.sunnysdk.com/v1/openapi.json The /v1 OpenAPI 3.1 document.

All four are public; no credential needed. Anything not in a published document isn’t part of the public API.

Every operation names the scope it needs. Two kinds of credential satisfy it.

For user-facing or multi-tenant integrations: OAuth 2.1, authorization code with PKCE. It is the same authorization server, scopes, and consent flow as the MCP server. Register a client (a Client ID Metadata Document is preferred), authorize with resource=https://api.sunnysdk.com, then send the token on every request. Without resource the token is opaque and the API rejects it. See OAuth & scopes for discovery, registration, and the full scope table.

Bearer token
curl https://api.sunnysdk.com/projects \
-H "Authorization: Bearer $TOKEN"

For server-to-server use without a browser sign-in, send a key in the x-api-key header. The plaintext key is shown once, when you create it.

  • Personal keys (sunny_sk_…) act as you. Create them under Account → API keys.
  • Workspace keys (sunny_wsk_…) are owned by the workspace, not a person, so they keep working when the member who created them leaves. A workspace owner or admin creates them under Organization → API keys.
API key
curl https://api.sunnysdk.com/projects \
-H "x-api-key: sunny_wsk_…"

You choose a key’s scopes when you create it. Personal keys can carry only the non-elevated action scopes. Workspace keys can also carry the workspace-level scopes; the ones marked opt-in are never selected by default and must be ticked on purpose.

ScopeWhat it allowsPersonal keyWorkspace key
projects:readView projectsYesYes
projects:writeEdit projectsYesYes
quotes:readView quotesYesYes
arrangeRun arrangementsYesYes
workflows:runRun render workflowsYesYes
workflows:readView render workflowsYesYes
catalog:readView catalogYesYes
docs:readRead product documentation—Yes
embeds:readView product embed analytics—Yes
embeds:writeManage product embeds—Yes
webhooks:manageManage webhook endpoints—Yes
quotes:writeEdit quotes—Yes
org:adminAdminister the workspace—Opt-in
audit:readRead the workspace audit log—Opt-in
quote_costs:readSee your dealer cost and margin—Opt-in
orders:writeImport order files—Opt-in
catalog:writeCurate the catalog—Opt-in

The admin scope can’t be put on a key, and sign-in scopes (openid, profile, email, offline_access) have no meaning on one.

Denials follow RFC 6750: 401 invalid_token for a missing, invalid, or expired credential, and 403 insufficient_scope when the credential lacks the operation’s scope (the WWW-Authenticate header names it). A credential that is present but wrong is never retried as anonymous.

These apply to the unversioned API. /v1 has its own conventions.

  • All dimensions are meters. The plan origin is top-left (x → right, y → down).
  • Reads and writes resolve against the caller’s active workspace. Another workspace’s id answers 404, indistinguishable on purpose from an id that doesn’t exist.
  • Unbounded lists are { items, nextCursor }. nextCursor is opaque: pass it back as the cursor query parameter. It is null on the last page. There is no total count.
  • Bounded lists are { items }, or a named plural (embeds, endpoints, typicals) where the route already ships that way.
  • Errors are a JSON body { error, detail? }.
  • Mutations that return no resource answer { ok: true }.
  • Catalog ids are opaque strings. Never derive a manufacturer from a prefix.
  • Layout payloads use the sunny-layout format.

The arrangement paths, POST /arrange and POST /plan-floor, return application/x-ndjson: one JSON object per line, progress lines first, then a terminal done line (with the generation id and final placements) or an error line. The HTTP status is 200 either way, so read the stream and inspect the last line. Runs can take up to about 160 seconds.

POST /quotes and POST /quotes/{id}/lines accept an Idempotency-Key header, as does every POST and PATCH under /v1. Send a key (1–255 printable ASCII characters; a random UUID is typical) on the first attempt and the same key on each retry. A retry of a finished request replays the stored response with Idempotent-Replayed: true; a retry while the first is still running gets 409 with Retry-After: 1; reusing a key for a different request gets 422. Keys are remembered for 24 hours.

beta

https://api.sunnysdk.com/v1 is the versioned API for integrations. It is curated: it grows one resource at a time, and each /v1 resource is a stable contract over the same data the unversioned API serves. The unversioned API is what the Sunny web app uses and it keeps evolving; build new integrations against /v1 where it covers what you need.

Every /v1 operation is currently beta (x-sunny-stability: beta in the document) and may still change shape. Once an operation leaves beta, changes to it are additive only, and a removal is announced with Deprecation and Sunset headers at least six months ahead.

Area Operations
Quotes List, create, get, update, delete; lines; presented versions; the Present link (get, mint, revoke, email)
Orders A quote’s order as JSON, Order SIF, or OFDA XML; import an order file as a draft quote
Customers List, create, get, update, delete
Discount tables List and get (quote_costs:read)
Catalog List and get catalog items; get a manufacturer
Audit events The workspace audit log (audit:read)
Sandbox Reset a sandbox workspace (org:admin, test key only)

The /v1 reference lists each operation’s scope.

An API key (x-api-key) or an OAuth Bearer token, and nothing else. A request with neither gets 401 with code credentials_missing. /v1 is server-to-server: browsers get no CORS allowance.

  • Ids are opaque strings; don’t parse them.
  • Fields are snake_case. Timestamps are ISO 8601 in UTC.
  • Money is an integer in minor units (cents for USD), with an ISO 4217 currency on the owning object.
  • A field with no value is omitted, never null.
  • Lists are { data, next_cursor? }. Pass next_cursor back as cursor; it is omitted on the last page. limit is 1–100, default 25.
  • Every response carries an X-Request-Id header. Quote it when reporting a problem.
  • Ignore response fields you don’t recognise, and handle enum values you don’t recognise, including error types and codes. That is what lets new fields and values ship without a new version.

Every /v1 error has one shape:

400 Bad Request
{
"error": {
"type": "invalid_request_error",
"code": "parameter_invalid",
"message": "limit: Too big: expected number to be <=100",
"param": "limit",
"request_id": "6f1c2a4e-1b7d-4d0e-9a51-1f0f6d3b8c22"
}
}
  • type is the broad class. The current set is invalid_request_error, authentication_error, permission_error, idempotency_error, rate_limit_error, and api_error; more may be added.
  • code is specific (parameter_invalid, insufficient_scope, not_found, rate_limited, …). Codes are added over time but never renamed or repurposed.
  • param is present only when one request field or header is at fault.
  • message is for people. Don’t parse it.
  • request_id matches the X-Request-Id header.

A workspace can create one sandbox companion under Organization → Sandbox, to build an integration without touching live data. Workspace keys created on the sandbox start with sunny_wsk_test_. A test key only works against its sandbox, a live key never works against a sandbox, and personal keys never act on a sandbox. The sandbox includes a fixture catalog and discount table, its email is logged rather than sent, and POST /v1/sandbox/reset clears its quotes, customers, orders, and discount tables and re-seeds the fixtures.

Every request made with an API key or an OAuth Bearer token is limited per owner over a 60-second window:

Bucket Limit per 60 s
Reads (GET, HEAD) 600
Writes (everything else) 120
Reads, sandbox workspace 120
Writes, sandbox workspace 30

Personal keys and Bearer tokens are counted per user (Bearer tokens also per OAuth client). Workspace keys are counted per workspace, so creating more keys does not raise the limit. /v1 and the unversioned API share the same buckets.

The cost endpoints — POST /arrange, POST /plan-floor, and minting a single-item embed — also allow 5 requests per 60 seconds per caller, on top of the limits above. The MCP server’s write and render tools draw from the same 5-request budget.

Limited responses carry these headers:

Header Value
RateLimit-Policy The bucket, quota, and window, e.g. "api-read";q=600;w=60.
RateLimit-Limit The bucket’s quota (read and write buckets).
RateLimit-Reset The window length in seconds — an upper bound on when the window resets.
RateLimit-Remaining Sent only on a 429, where it is 0.
RateLimit On a 429, e.g. "api-read";r=0;t=60.
Retry-After On a 429: 60.

A rejection is 429 with {"error":"rate_limited"} on the unversioned API, or the error envelope with code rate_limited on /v1. Counters are approximate, so back off on a 429 rather than pacing to the exact quota.