Skip to main content

API v1

Spoonjoy Developer Platform

Build clients on Spoonjoy's public-by-default Chef graph, then add scoped auth only when a workflow needs private shopping-list state, token management, or delegated access.

Version

v1

Base path

REST v1

Spec

Machine-readable

Errors

13 codes

Terminal Quickstart

curl + jq

First successful calls

Anonymous public reads work immediately. For the first external token, use delegated approval: the client shows an approval URL, the chef signs into Spoonjoy, and polling returns a one-time-display bearer token.

export SJ_BASE='https://spoonjoy.app'
umask 077
state_dir="$(mktemp -d "${TMPDIR:-/tmp}/spoonjoy.XXXXXX")"
trap 'rm -rf "$state_dir"' EXIT

curl -fsS "$SJ_BASE/api/v1/health" | jq
curl -fsS "$SJ_BASE/api/v1/recipes?query=pasta&limit=5" \
  | jq -r '.data.recipes[] | [.id, .title] | @tsv'

# Public catalog calls omit Authorization. Use the delegated token below
# for private shopping-list calls, not for every public recipe request.
# First external shopping-list token without browser cookies: delegated approval
curl -fsS "$SJ_BASE/api/tools/start_agent_connection" \
  -H 'Content-Type: application/json' \
  --data '{"agentName":"Kitchen CLI","scopes":"shopping_list:read shopping_list:write"}' \
  > "$state_dir/start.json"

jq -r '.data.authorizationUrl' "$state_dir/start.json"
jq -r '.data.verificationUri' "$state_dir/start.json"
jq -r '.data.userCode' "$state_dir/start.json"
jq -r '.data.deviceCode' "$state_dir/start.json"

while :; do
  curl -fsS "$SJ_BASE/api/tools/poll_agent_connection" \
    -H 'Content-Type: application/json' \
    --data "{\"deviceCode\":\"$(jq -r '.data.deviceCode' "$state_dir/start.json")\"}" \
    > "$state_dir/poll.json"
  status="$(jq -r '.data.status' "$state_dir/poll.json")"
  test "$status" = approved && break
  if test "$status" != pending; then
    jq . "$state_dir/poll.json" >&2
    echo "Connection status is $status; start a new delegated approval request if you still need access." >&2
    exit 1
  fi
  sleep "$(jq -r '.data.interval // 2' "$state_dir/start.json")"
done

export SPOONJOY_TOKEN="$(jq -r '.data.token' "$state_dir/poll.json")"

Current API Boundary

Available now

  • - public recipe and cookbook reads
  • - owner-scoped shopping-list read/sync/write
  • - session-created and bearer-created API tokens
  • - OAuth/PKCE delegated access
  • - delegated agent/device approval links
  • - remote MCP endpoint
  • - cursor-paginated public recipe and cookbook lists

Not in v1 yet

  • - Recipe write, import, or export endpoints
  • - Private recipe-library endpoints
  • - Inventory or pantry stock APIs
  • - Meal plan or "today's recipes" APIs
  • - Full account export APIs
  • - Canonical unit registry or density-based ingredient conversion
  • - webhooks, REST Hooks, SSE, and event subscriptions
  • - Bulk shopping-list import or batch mutation endpoints

Corporate tenant/admin APIs, inventory, meal plans, full exports, canonical unit conversion, webhooks, REST Hooks, batch mutations, and recipe write/import/export endpoints are future API surface, not hidden current endpoints.

Delegated approval helper endpoints under /api/tools/* are part of the current public connection flow. Other legacy app-only /api/* routes are not the external contract.

Legacy /api/* routes reject OAuth access tokens that are audience-bound to /mcp. New external REST clients should use /api/v1/*.

External Client Guide

Client examples

  • Tiny-device clients: Use sync cursors, small payloads, and idempotent retries when a device is offline or battery constrained.
  • Mobile apps: Read public recipes without auth, then request shopping-list scopes only after a chef connects their account.
  • CLI/script clients: Use bearer credentials, curl, and OpenAPI JSON only when the script cannot share a Spoonjoy session.
  • Browser clients: Use same-origin Session only inside spoonjoy.app. Extensions and third-party browser apps use OAuth/PKCE.
  • Agent clients: Use MCP or delegated connection endpoints when a chef needs to approve an assistant-style runtime.
  • Enterprise clients: API v1 is individual delegated access today; there are no tenant, admin, employee, or org-export APIs yet.

No token required

Read the public Chef graph

Start with GET /api/v1/recipes and GET /api/v1/cookbooks. Public graph reads do not require Authorization: Bearer, and the OpenAPI contract is available at /api/v1/openapi.json.

curl 'https://spoonjoy.app/api/v1/recipes?query=pasta&limit=20'
curl 'https://spoonjoy.app/api/v1/cookbooks?limit=20'

Requires login

Use your Spoonjoy session

Sign into Spoonjoy, open the playground, and leave auth on Session. There is no token to mint or paste for playground calls; the browser sends your normal Spoonjoy session cookie, and private endpoints treat that as the authenticated chef.

https://spoonjoy.app/api/playground

External clients

Use bearer only outside the session

Bearer mode is for clients that cannot use the logged-in Spoonjoy browser session. The generated POST /api/v1/tokens operation is available in the playground because it is part of API v1, not because private playground calls need a separate token.

Playground auth: Session
Generated operation: POST /api/v1/tokens
Use the returned sj_... secret only in an external client or Bearer-mode test.

Requires shopping_list:read

Sync a private shopping list

Use GET /api/v1/shopping-list/sync with an opaque cursor to fetch active rows and deletion records for removed rows. Omit cursor on the first request, then store nextCursor only after your client applies the whole response.

curl -fsS 'https://spoonjoy.app/api/v1/shopping-list/sync' \
  -H 'Authorization: Bearer sj_client_token'

Requires shopping_list:write

Perform an idempotent shopping-list mutation

Use POST /api/v1/shopping-list/items with clientMutationId so retries can replay the same write without duplicating items.

curl -fsS -X POST 'https://spoonjoy.app/api/v1/shopping-list/items' \
  -H 'Authorization: Bearer sj_client_token' \
  -H 'Content-Type: application/json' \
  -d '{"clientMutationId":"device-uuid-1","name":"Eggs","quantity":12,"unit":"Each"}'

Token Acquisition

Same-origin

No token: signed-in browser

A same-origin browser client does not fetch or store a bearer token. The chef signs into Spoonjoy with password, passkey, or any configured Google, GitHub, or Apple provider, and private API calls use the resulting session cookie. Those provider buttons are Spoonjoy sign-in methods, not OAuth providers that your client owns.

Login surface: /login
Then call: fetch("/api/v1/shopping-list", { credentials: "same-origin" })

Direct token

Personal token: signed-in chef creates one

For a script, device, or developer-owned client, the chef signs in first and runs POST /api/v1/tokens from Session auth, such as through the generated playground. An existing bearer credential with tokens:write can also create another token, but never with broader scopes than it already has. Spoonjoy returns the raw sj_... secret once; save it outside browser bundles.

POST /api/v1/tokens
Auth: Session cookie or Bearer sj_... with tokens:write
Body: { "name": "Kitchen script", "scopes": ["recipes:read", "cookbooks:read", "shopping_list:read", "shopping_list:write"] }
Response: { "ok": true, "data": { "token": "sj_...", "credential": { "id": "cred_...", "scopes": [...] } } }

Third-party

Delegated token: OAuth/PKCE

For a third-party app, register a public client and redirect the chef to /oauth/authorize. If they are not signed in, Spoonjoy routes them through /login and the full auth surface before consent. The client never handles the chef's password. The client exchanges the authorization code at /oauth/token for an sj_... access_token plus rotating refresh_token.

POST /oauth/register -> client_id: cm_client_id_from_register
GET /oauth/authorize?client_id=cm_client_id_from_register&redirect_uri=...&response_type=code&scope=shopping_list%3Aread+shopping_list%3Awrite&state=...&code_challenge=...&code_challenge_method=S256
POST /oauth/token -> access_token: sj_...
POST /oauth/revoke -> revoke refresh_token and matching live OAuth access credentials

Agent/device

Delegated token: approval link

For clients that cannot run a browser-based OAuth callback, call POST /api/tools/start_agent_connection, show the authorizationUrl or stable verificationUri plus userCode to the chef, then poll POST /api/tools/poll_agent_connection no faster than the returned interval. Pass scopes for least privilege, such as shopping_list:read shopping_list:write; omitted scopes default to the same shopping-list read/write pair. The approval page also uses Spoonjoy's full login surface before issuing a one-time-display sj_... bearer token plus credential id. Personal and delegated bearer tokens do not expire unless expiresAt is non-null; rerun approval when a stored token returns 401 invalid_token. A device can revoke its own credential id with DELETE /api/v1/tokens/{credentialId}; revoking any other token requires tokens:write.

POST /api/tools/start_agent_connection -> verificationUri + verificationUriComplete + authorizationUrl + userCode + deviceCode + expiresIn: 600
POST /api/tools/poll_agent_connection -> status: pending | approved | denied | expired | claimed
Approved response -> token: sj_... + credential metadata, including scopes and expiresAt

Security

No password-token API

Spoonjoy does not support an OAuth password grant or API endpoint where a third-party client trades a chef's password for a token. Email/password login creates a session cookie, not an API token. Clients should use OAuth/PKCE or delegated approval so Spoonjoy, not the client, handles password, passkey, and provider login.

Do not implement: grant_type=password
Use instead: OAuth/PKCE or delegated approval link

Auth Implementation

Browser

Same-origin browser session

After a chef signs in, your logged-in Spoonjoy session is the credential. Call relative /api/v1 URLs with credentials: "same-origin". Do not send Authorization; if an Authorization header is present, bearer auth wins over the session.

await fetch("/api/v1/shopping-list", {
  credentials: "same-origin",
  headers: { "X-Request-Id": "web-shopping-list" },
});

Bearer

External REST client

Use bearer only when a client cannot share the logged-in Spoonjoy session. In the playground, leave auth on Session and run the generated POST /api/v1/tokens operation, then store the sj_... secret outside browser bundles. Bearer-created tokens inherit the caller's scopes by default. Bearer callers cannot create a token with broader scopes than they already have.

curl 'https://spoonjoy.app/api/v1/shopping-list' \
  -H 'Authorization: Bearer sj_client_token' \
  -H 'X-Request-Id: client-shopping-list'

Delegated

OAuth/PKCE app

Register a public client with token_endpoint_auth_method: none and no client secret, redirect the chef through consent, then exchange the single-use 60-second code with a form-encoded POST /oauth/token request. Registration accepts common RFC 7591/OIDC metadata, but Spoonjoy stores only client_name and exact redirect_uris today. Optional scope metadata can be validated at registration, but the authorize request scope is the grant. Use a 43-128 character high-entropy PKCE verifier, S256 code challenge, and state. If the chef is not signed in, Spoonjoy routes them through /login first, where password, passkey, and configured Google, GitHub, or Apple sign-in all return to consent. OAuth accepts kitchen scopes plus least-privilege public, recipe, cookbook, and shopping-list scopes. The returned sj_... access_token lasts 15 minutes (expires_in: 900), refresh_token rotates on every refresh grant, and POST /oauth/revoke disconnects the stored refresh token plus live OAuth access credentials for that client/resource.

POST /oauth/register
Response: { "client_id": "cm_client_id_from_register" }
GET /oauth/authorize?client_id=cm_client_id_from_register&redirect_uri=https%3A%2F%2Fexample.com%2Foauth%2Fcallback&response_type=code&scope=shopping_list%3Aread+shopping_list%3Awrite&state=...&code_challenge=...&code_challenge_method=S256
POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&client_id=...&redirect_uri=https%3A%2F%2Fexample.com%2Foauth%2Fcallback&code=...&code_verifier=...

POST /oauth/revoke
token=ort_...&client_id=cm_client_id_from_register&token_type_hint=refresh_token

Errors

Auth failures

Treat authentication_required and invalid_token as 401 responses, insufficient_scope as 403, and malformed Authorization headers as validation_error. Public endpoints can be anonymous; Omit `Authorization` on public calls unless you require authenticated behavior. If you send credentials to an optional endpoint, Spoonjoy validates them and checks scopes. Log requestId or X-Request-Id for support.

{
  "ok": false,
  "requestId": "client-shopping-list",
  "error": { "code": "insufficient_scope", "status": 403 }
}

Response Protocols

API v1 REST response shape

/api/v1 REST resources return ok, requestId, and data or error fields. Raw OpenAPI documents are the exception.

{ "ok": true, "requestId": "req_example", "data": {} }
{ "ok": false, "requestId": "req_example", "error": { "code": "invalid_token", "status": 401 } }

Protocol exceptions

OAuth endpoints use OAuth token/error JSON except 429 rate limits, delegated /api/tools/* helpers use the legacy helper envelope, and /mcp uses JSON-RPC result/error objects.

OAuth: { "access_token": "sj_...", "refresh_token": "ort_..." }
Agent helper: { "ok": true, "data": { "status": "pending" } }
MCP: { "jsonrpc": "2.0", "result": {} }

OAuth And Delegated Flows

Browser clients may call /oauth/register, /oauth/token, and /oauth/revoke cross-origin; those endpoints answer OPTIONS with CORS headers. Cookie-authenticated API mutations remain same-origin session calls and should never rely on copied cookies in external clients.

Do not request `offline_access` in OAuth authorize. OAuth clients can request kitchen scopes or least-privilege public, recipe, cookbook, and shopping-list scopes; refresh tokens are returned by the authorization-code flow.

Mobile, SaaS, extension

OAuth/PKCE delegated app

Use for third-party apps that can receive a redirect callback and should never handle a chef password.

/.well-known/oauth-authorization-server -> /oauth/register -> /oauth/authorize -> /oauth/token -> /oauth/revoke

  • - Dynamic client registration is public and returns token_endpoint_auth_method: none.
  • - Redirect URIs must be HTTPS; HTTP is accepted only for localhost and 127.0.0.1.
  • - PKCE is required: use a 43-128 character code_verifier and S256 code_challenge.
  • - Authorization codes are single-use and expire after 60 seconds.
  • - Access tokens are sj_... bearer credentials with a 900-second lifetime.
  • - Refresh tokens are ort_... values and rotate on every refresh grant; POST /oauth/revoke disconnects a stored refresh token and revokes live OAuth access credentials for that client/resource.
  • - Registration validates optional scope metadata but does not grant it; the authorize request scope is the grant. Blank authorize scope defaults to kitchen:read, but apps should send explicit least-privilege scopes.
  • - OAuth scopes never grant tokens:read or tokens:write; personal token management uses /api/v1/tokens.
  • - grant_type=password is never supported.
  • - Most OAuth errors use standard OAuth JSON. Rate-limit responses are Spoonjoy's generic rate-limit shape with Retry-After.
curl -sS 'https://spoonjoy.app/oauth/register' \
  -H 'Content-Type: application/json' \
  --data '{"client_name":"Example grocery app","redirect_uris":["https://example.com/oauth/callback"],"token_endpoint_auth_method":"none"}'

# Open this URL in the browser after generating a PKCE S256 challenge.
open 'https://spoonjoy.app/oauth/authorize?client_id=cm_client_id_from_register&redirect_uri=https%3A%2F%2Fexample.com%2Foauth%2Fcallback&response_type=code&scope=shopping_list%3Aread+shopping_list%3Awrite&state=...&code_challenge=...&code_challenge_method=S256'

curl -sS 'https://spoonjoy.app/oauth/token' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data 'grant_type=authorization_code&client_id=cm_client_id_from_register&redirect_uri=https%3A%2F%2Fexample.com%2Foauth%2Fcallback&code=oac_...&code_verifier=pkce_verifier_0123456789_abcdefghijklmnopqrstuvwxyz_ABCDEF'

curl -sS -X POST 'https://spoonjoy.app/oauth/revoke' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data 'token=ort_...&client_id=cm_client_id_from_register&token_type_hint=refresh_token'

Agent, appliance, no callback

Delegated approval link

Use for agents, CLIs, kitchen displays, and constrained devices that can show a chef an approval URL but cannot run an OAuth callback.

/api/tools/start_agent_connection -> /api/tools/poll_agent_connection -> /api/v1/tokens/{credentialId}

  • - The device code expires after 10 minutes.
  • - Poll no faster than the returned interval, currently 2 seconds.
  • - A pending poll returns pending plus authorizationUrl, verificationUri, verificationUriComplete, and userCode.
  • - Pass scopes such as shopping_list:read shopping_list:write to request a least-privilege delegated token; omitted scopes default to shopping_list:read shopping_list:write.
  • - Tiny devices can show verificationUri plus userCode instead of the long authorizationUrl.
  • - An approved poll returns the sj_... token once, plus token metadata.
  • - The token is a normal bearer credential. A device can revoke its own credential id with DELETE /api/v1/tokens/{credentialId}; revoking any other credential requires tokens:write.
SJ_BASE='https://spoonjoy.app'
umask 077
state_dir="$(mktemp -d "${TMPDIR:-/tmp}/spoonjoy.XXXXXX")"
trap 'rm -rf "$state_dir"' EXIT

curl -sS 'https://spoonjoy.app/api/tools/start_agent_connection' \
  -H 'Content-Type: application/json' \
  --data '{"agentName":"Kitchen display","scopes":"shopping_list:read shopping_list:write"}' \
  > "$state_dir/start.json"

echo "Open $(jq -r '.data.verificationUri' "$state_dir/start.json") and enter $(jq -r '.data.userCode' "$state_dir/start.json")"

while :; do
  curl -sS "$SJ_BASE/api/tools/poll_agent_connection" \
    -H 'Content-Type: application/json' \
    --data "{\"deviceCode\":\"$(jq -r '.data.deviceCode' "$state_dir/start.json")\"}" \
    > "$state_dir/poll.json"
  status=$(jq -r '.data.status' "$state_dir/poll.json")
  test "$status" = approved && break
  test "$status" = pending || exit 1
  sleep "$(jq -r '.data.interval // 2' "$state_dir/start.json")"
done
export SPOONJOY_TOKEN="$(jq -r '.data.token' "$state_dir/poll.json")"

Assistant runtime

Remote MCP client

Use for MCP-capable clients that discover OAuth metadata, get a bearer token, then call the remote Spoonjoy MCP endpoint.

/mcp -> /.well-known/oauth-protected-resource -> /.well-known/oauth-authorization-server

  • - POST /mcp challenges unauthenticated callers with OAuth protected-resource metadata.
  • - MCP delegated scopes use kitchen:read and kitchen:write.
  • - Token-management tools require personal tokens:read or tokens:write scopes; OAuth kitchen scopes do not grant them.
  • - The approved bearer token must be sent as Authorization: Bearer sj_...
curl -sS 'https://spoonjoy.app/mcp' \
  -H 'Authorization: Bearer sj_...' \
  -H 'Content-Type: application/json' \
  --data '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

OAuth Scope Mapping

kitchen:read

cookbooks:read + public:read + recipes:read + shopping_list:read

kitchen:write

shopping_list:write

shopping_list:read

shopping_list:read

shopping_list:write

shopping_list:write

recipes:read

recipes:read

cookbooks:read

cookbooks:read

public:read

recipes:read + cookbooks:read

OAuth clients may request the delegated scopes above directly. Token-management scopes remain personal-token or Session-only and are not granted by OAuth.

Scenario Quickstarts

iOS + Android

Native mobile OAuth

Register once per app install or environment, persist client_id, and do not register on every launch. Use HTTPS universal links or Android App Links for production callbacks, with localhost or 127.0.0.1 loopback only for development. Store tokens in Keychain or Android Keystore-backed storage. Refresh tokens rotate, so replace the stored refresh token atomically and use single-flight refresh when concurrent requests hit 401.

POST /oauth/register
{ "client_name": "Grocery helper", "redirect_uris": ["https://example.com/spoonjoy/oauth/callback"], "token_endpoint_auth_method": "none" }

GET /oauth/authorize?...&state=client_state&code_challenge=pkce_s256&scope=shopping_list:read+shopping_list:write

POST /oauth/token
grant_type=authorization_code&client_id=cm_client_id_from_register&redirect_uri=https%3A%2F%2Fexample.com%2Fspoonjoy%2Foauth%2Fcallback&code=oac_...&code_verifier=pkce_verifier_...

POST /oauth/token
grant_type=refresh_token&client_id=cm_client_id_from_register&refresh_token=ort_...

GET /api/v1/shopping-list/sync?limit=50

Extension

Browser extension OAuth

API v1 does not create or import recipes yet; the supported extension story today is shopping-list ingredient sync. Run OAuth/PKCE in the extension background, persist client_id plus state and code_verifier until callback, verify state, and make bearer API calls from the background instead of a content script. Register the HTTPS callback from chrome.identity.getRedirectURL/launchWebAuthFlow exactly; custom extension schemes are rejected.

const item = { sourceRowId: "row-42", name: "Eggs", quantity: 12, unit: "Each" }
clientMutationId = `extension:${sha256(recipeUrl)}:${item.sourceRowId}:${bodyHash}`
POST /api/v1/shopping-list/items
Authorization: Bearer sj_...
{ "clientMutationId": "extension:...", "name": "Eggs", "quantity": 12, "unit": "Each" }

CLI/script

Cron shopping-list export/import

Omit cursor on the first sync, then store data.nextCursor after applying every item and tombstone. Use limit for small payloads and keep fetching while hasMore is true. Import with deterministic chef-wide mutation ids such as shopping-import:<source-system>:<source-row-id>:<body-hash>; this is not a durable set-desired-state API after the 24-hour idempotency window.

umask 077
tmp_dir="$(mktemp -d "${TMPDIR:-/tmp}/spoonjoy-sync.XXXXXX")"
trap 'rm -rf "$tmp_dir"' EXIT
curl -fsS 'https://spoonjoy.app/api/v1/shopping-list/sync?limit=20' \
  -H 'Authorization: Bearer sj_...' > "$tmp_dir/sync.json"
jq -n --arg cursor "$(jq -r '.data.nextCursor' "$tmp_dir/sync.json")" '{cursor:$cursor}' > state.json

Serverless

Cloudflare Worker sync bridge

Store access_token, rotating refresh_token, and cursor state behind a Durable Object, queue-level serializer, or D1 row lock. KV is fine for read-through snapshots after the lock, but not as the refresh-token compare-and-set mechanism. Respect Retry-After and never store authenticated bearer responses in caches.default.

Queue message: { chefId, mutation, clientMutationId }
Durable Object per chef: refresh once, atomically store the rotated refresh_token, then retry queued mutations with the same clientMutationId.
GET /api/v1/shopping-list/sync?limit=50
Authorization: Bearer sj_...

Zapier + Make

No-code connector profile

Import /api/v1/openapi.connector.json for an OpenAPI 3.0 REST-only profile. Configure OAuth as public client, no client secret, PKCE S256, authorization URL /oauth/authorize, token and refresh URL /oauth/token, revoke URL /oauth/revoke. API v1 has no webhooks yet; expose shopping-list sync as a polling trigger.

Trigger: New, updated, or removed shopping-list item
1. GET /api/v1/shopping-list/sync?limit=50
2. Sort returned rows by updatedAt descending for the no-code platform.
3. Dedupe events by id:updatedAt so changed items are not suppressed.
4. Treat deletedAt rows as removals or filtered tombstones.
5. Persist nextCursor only after the run succeeds.

Reporting

Public BI snapshot export

Public recipe/cookbook lists use createdAt/id cursor walks. They are not repeatable snapshot guarantees or updatedAt incremental exports, and they do not include deletion tombstones; restart a full crawl when you need to catch public edits or removals. Preserve attribution, respect Retry-After, and do not treat public JSON as a photo-copying or commercial republication license.

snapshot_resource recipes recipes recipes.ndjson
snapshot_resource cookbooks cookbooks cookbooks.ndjson

GET /api/v1/recipes?limit=50
GET /api/v1/cookbooks?limit=50
GET /api/v1/recipes?cursor=v1.cursor_from_nextCursor&limit=50

Anonymous public responses may include Cache-Control: public, max-age=60, stale-while-revalidate=300 but not ETag or Last-Modified.

REST-powered embeds only

Recipe blog embeds

Fetch public JSON and render your own HTML with textContent, not innerHTML. Spoonjoy pages are not iframe embeds. Recipe steps are returned in ascending stepNum order, step duration is minutes when present, and ingredients are step-attached in API order. Validate sourceUrl as http/https before linking, write your own image alt text, and avoid copying photos where removals cannot be honored.

GET /api/v1/recipes/{id}
If 404 not_found, hide or replace the embed before rendering stale content.
Render servings, step ingredients, ordered steps, and attribution.creditText as a link to attribution.canonicalUrl.
If attribution.sourceRecipe.deleted is true, credit it as unavailable instead of linking it.

Reference

root

/api/v1

GETAnonymous

Anonymous callers allowed; authenticated callers are scope checked.

health

/api/v1/health

GETAnonymous

Anonymous callers allowed; authenticated callers are scope checked.

openapi

/api/v1/openapi.json

GETAnonymous

Anonymous callers allowed; authenticated callers are scope checked.

openapi-sdk

/api/v1/openapi.sdk.json

GETAnonymous

Anonymous callers allowed; authenticated callers are scope checked.

openapi-connector

/api/v1/openapi.connector.json

GETAnonymous

Anonymous callers allowed; authenticated callers are scope checked.

recipes

/api/v1/recipes

GETrecipes:read

Anonymous callers allowed; authenticated callers are scope checked.

recipe

/api/v1/recipes/{id}

GETrecipes:read

Anonymous callers allowed; authenticated callers are scope checked.

cookbooks

/api/v1/cookbooks

GETcookbooks:read

Anonymous callers allowed; authenticated callers are scope checked.

cookbook

/api/v1/cookbooks/{id}

GETcookbooks:read

Anonymous callers allowed; authenticated callers are scope checked.

shopping-list

/api/v1/shopping-list

GETshopping_list:read

Authenticated chef surface.

shopping-list-sync

/api/v1/shopping-list/sync

GETshopping_list:read

Authenticated chef surface.

shopping-list-items

/api/v1/shopping-list/items

POSTshopping_list:write

Authenticated chef surface.

shopping-list-item

/api/v1/shopping-list/items/{itemId}

PATCHshopping_list:writeDELETEshopping_list:write

Authenticated chef surface.

tokens

/api/v1/tokens

GETtokens:readPOSTtokens:write

Authenticated chef surface.

token

/api/v1/tokens/{credentialId}

DELETEtokens:write

Authenticated chef surface.

Scopes

public:read

Public read

kitchen:read

Delegated kitchen read

kitchen:write

Delegated kitchen write

recipes:read

Recipe graph read

cookbooks:read

Cookbook graph read

shopping_list:read

Shopping list read

shopping_list:write

Shopping list write

tokens:read

Token metadata read

tokens:write

Bearer credential create and revoke

Auth

Spoonjoy session

Best for the playground and same-origin browser clients. Sign in once; your login is the credential for private API calls.

Bearer credentials

Best only when a client runs outside the Spoonjoy browser session. The token API is exposed as part of the generated surface.

OAuth/PKCE apps

Best for third-party apps. Dynamic registration, authorize, and token routes are exposed for delegated consent.

MCP clients

Best for assistant-style clients that need a tool connection instead of raw REST calls.

Delegated and device-style authorization

Best for clients that need a chef to approve access from a constrained device or external runtime.

Sync And Safety

Recipe and cookbook lists are public catalog search endpoints today. The query parameter wins when both query and q are supplied; lists include cursor, nextCursor, hasMore, cover images, canonicalUrl, and attribution fields. Owner export and deleted recipe tombstones are not in API v1 yet.

Shopping-list cursor sync is the current incremental owner-data path. Store nextCursor after applying each batch and retry mutations with stable clientMutationId values.

Recipe ingredient quantities, units, servings, temperatures, and timers are original author data in API v1. Units are free-form display strings, not a canonical conversion model; there is no /api/v1/units registry or density table yet.

API v1 is rate limited by IP and credential before authentication work. Rate-limited responses return 429 and Retry-After; configured limits are edge abuse protection, not a globally precise quota meter.

Cursor

Use the returned nextCursor as the next request cursor after applying every item in the page durably. Treat it as opaque; ISO timestamps are accepted only as a bootstrap convenience.

Tombstones

Sync includes deleted rows with deletedAt so offline clients can remove local items.

Pagination

Use limit from 1 to 50 for small payloads. hasMore: true means continue with the returned nextCursor; webhooks, REST Hooks, SSE, and event subscriptions are not available yet.

Idempotent shopping-list mutations

clientMutationId is scoped to the chef, retained for 24 hours, and bound to method, path, and body hash. Persist and retry the exact serialized body for that mutation id.

Replay

Retry the same request with the same clientMutationId after a timeout; Spoonjoy returns the recorded response with mutation.replayed: true.

Conflict

Reusing the same clientMutationId for a different method, path, or body returns 409 idempotency_conflict.

Retries

Retry network timeouts, 429, and 5xx responses with the same mutation id. Refresh or reconnect on 401. Do not retry validation, scope, or idempotency conflicts unchanged.

Sample flow

  1. Read public recipes and cookbooks anonymously before adding auth.
  2. Use Session for logged-in playground calls; use bearer or OAuth only when a client runs outside that session.
  3. Use a stable mutation id for shopping-list writes, then retry with the same value when a network call is interrupted.
  4. Use the sync cursor to fetch shopping-list changes, including removed items.

Client Starting Points

Public catalog

GET /api/v1/recipes and GET /api/v1/cookbooks need no token.

Private list

Use shopping-list read and write scopes for pantry-style clients.

Machine errors

Every v1 error returns ok false, requestId, code, message, and status.