API reference
Every endpoint, grouped by area, from the OpenAPI document the API serves: https://dev-api.pluzzento.com/v1/openapi.json
304 operations, API version 1.
admin
get/v1/admin/audit-logs
Parameters:
limit(query),cursor(query),filter[action](query),filter[resource](query),filter[actor](query),filter[impersonator](query),filter[targetType](query),filter[targetId](query),filter[from](query),filter[to](query)Responses: 200, 400, 401, 403, default
get/v1/admin/audit-logs/{id}
Parameters:
id(path, required)Responses: 200, 401, 403, 404, default
get/v1/admin/entitlement-overrides
Parameters:
limit(query),cursor(query),filter[subjectType](query),filter[subjectId](query),filter[key](query),filter[active](query)Responses: 200, 400, 401, 403, default
post/v1/admin/entitlement-overrides
Grant an entitlement override
Responses: 201, 400, 401, 403, 404, default
Details
The newest unexpired override of a key wins over the plan value. The subject must exist (404 `SUBJECT_NOT_FOUND`). Audited.
post/v1/admin/entitlement-overrides/{id}/revoke
Revoke (expire now) an override; audited
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, 409, default
post/v1/admin/impersonations
Start impersonating a user
Responses: 201, 400, 403, 404, 422, default
Details
Support and super admins only, from their own session reauthenticated within the last 5 minutes. Opens a separate session for the user (at most 30 minutes; tokens carry an `act` claim) and sets it as the auth cookies; the staff refresh token is kept aside so `DELETE /v1/users/me/impersonation` returns to it. In that session credential, billing, delete and staff operations answer 403 `IMPERSONATION_FORBIDDEN`. Audited.
get/v1/admin/organizations
Parameters:
limit(query),cursor(query),filter[q](query),filter[status](query),filter[type](query),filter[owner](query)Responses: 200, 400, 401, 403, 404, default
get/v1/admin/organizations/{id}
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, default
post/v1/admin/organizations/{id}/disable
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, default
post/v1/admin/organizations/{id}/enable
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, default
post/v1/admin/organizations/{id}/rename
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, 409, default
get/v1/admin/plans
Responses: 200, 401, 403, default
patch/v1/admin/plans/{key}
Change a plan’s entitlements or availability
Parameters:
key(path, required)Responses: 200, 400, 401, 403, 404, 422, default
Details
Lowering limits never deletes content; consumers restrict. Audited with the reason and each changed value.
get/v1/admin/settings
Responses: 200, 401, 403, default
patch/v1/admin/settings
Change global settings
Responses: 200, 400, 401, 403, default
Details
Partial: only the fields sent change. Audited with the reason and the field names (never the addresses; the registration allow-list only by its size).
get/v1/admin/tickets
Parameters:
limit(query),cursor(query),filter[status](query),filter[category](query),filter[assignee](query),filter[requester](query)Responses: 200, 400, 401, 403, default
get/v1/admin/tickets/{id}
Parameters:
id(path, required)Responses: 200, 401, 403, 404, default
patch/v1/admin/tickets/{id}
Change a ticket’s status or assignment
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, 409, 422, default
Details
open → in_progress ⇄ waiting_on_user → resolved → closed (in_progress and waiting_on_user may both resolve). Closed tickets are read-only (409 `TICKET_NOT_OPEN`). Assignees must be active support staff.
post/v1/admin/tickets/{id}/messages
Reply or add an internal note
Parameters:
id(path, required)Responses: 201, 400, 401, 403, 404, 409, 422, default
Details
A public reply is mailed to the requester and starts work on an open ticket; an internal note is visible to staff only.
get/v1/admin/users
Parameters:
limit(query),cursor(query),filter[q](query),filter[status](query),filter[staffRole](query),filter[deleted](query)Responses: 200, 400, 401, 403, default
get/v1/admin/users/{id}
Parameters:
id(path, required)Responses: 200, 401, 403, 404, default
post/v1/admin/users/{id}/restore
Restore a suspended user (idempotent, audited)
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, 422, default
put/v1/admin/users/{id}/staff-roles
Replace a user’s staff roles
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, 422, default
Details
Super admins only, never on their own account (422 `STAFF_SELF_ACTION`); the last active super admin keeps the role (422 `LAST_SUPER_ADMIN`). Audited with the reason, the roles granted and revoked.
post/v1/admin/users/{id}/suspend
Suspend a user
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, 422, default
Details
Signs out every session of the user (impersonations included); suspended accounts get 403 `ACCOUNT_DISABLED`. Idempotent; never your own account. Audited with the reason.
get/v1/users/me/impersonation
Whether this session is an impersonation
Responses: 200, 401, 403, default
Details
For the impersonation banner: `active` with the session details while a staff member impersonates the signed-in user, else `active: false`.
delete/v1/users/me/impersonation
End the impersonation
Responses: 200, 401, 404, default
Details
Closes the impersonation this session belongs to and revokes its session (audited), then signs the staff member back into their own session from the staff refresh cookie set at the start (`staffSessionRestored`); otherwise the auth cookies are cleared.
analytics
post/v1/analytics/events
Send a batch of play events
Responses: 202, 400, 413, 429, default
Details
At most 50 events, 64 KiB. Without analytics consent (the current consent record of the account, or of `anonymousId` when signed out) events are stored without any player identifier. Events are kept only about publications the caller could play: published public or unlisted ones that aren't embed-only; embed-only and private ones with a valid `embedToken` for them; private ones for signed-in members with `puzzle:read`, access grantees and organization devices in scope. Events about school organizations' publications never carry a player identifier. Others, invalid events and events older than 30 days (or the raw retention) are dropped alike (`rejected`), so the counts never reveal which publications exist.
get/v1/analytics/exports
A workspace's analytics exports, newest first
Parameters:
limit(query),cursor(query),ownerOrgId(query)Responses: 200, 400, 401, 403, 404, default
Details
Every export of the workspace (whoever asked), all statuses. Keyset by id.
post/v1/analytics/exports
Request an analytics export (CSV or JSON)
Responses: 202, 400, 401, 403, 404, 422, 429, default
Details
Queues a CSV or JSON file of one report, with exactly the rules of the query API (counts only; unique players rounded down to multiples of 5 and suppressed below 5; details suppressed below 5 starts; ranges clamped to the plan's retention; live days lag an hour). Built asynchronously into private storage: poll `GET /v1/analytics/exports/{id}` (or wait for the `analytics.export_ready` notification and mail), then `POST …/download` for a 5-minute link. Files are kept for the platform's export retention (7 days by default). Per workspace: 3 building at once and 20 per rolling 24 hours (429 `ANALYTICS_EXPORT_RATE_LIMITED`, `details { reason: concurrent|daily, limit, retryAfterSeconds }`). `publications` exports hold at most 10000 rows (`truncated`). CSV cells that start with `=`, `+`, `-`, `@`, a tab or a carriage return are prefixed with `'`. API keys with the `analytics:read` scope may call it. Refused under staff impersonation (403 `IMPERSONATION_FORBIDDEN`); listing, reading and downloading stay allowed.
get/v1/analytics/exports/{id}
One analytics export and its status
Parameters:
id(path, required)Responses: 200, 401, 403, 404, default
post/v1/analytics/exports/{id}/download
Get a download link for a ready analytics export
Parameters:
id(path, required)Responses: 200, 401, 403, 404, 409, 410, default
Details
A signed URL of the private file, valid for 5 minutes (`attachment; filename="analytics-<report>-<from>-<to>.<format>"`); ask again for a new one. Audited.
get/v1/analytics/overview
Workspace analytics report
Parameters:
from(query),to(query),origin(query),ownerOrgId(query)Responses: 200, 400, 401, 403, 404, 422, default
Details
Counts only (no player identifiers). Range: inclusive UTC days clamped to the plan's aggregated retention and today; today and days not aggregated yet are computed live. Unique players: consenting players only, exact while raw events are kept; counts of 1–4 are suppressed; never collected for school organizations. Origins are indicative only: events of publications played without an embed token carry the origin the client claims (`embedOrigin`), which nobody verifies; only token embeds record the token's verified origin. API keys with the `analytics:read` scope may call it. `private, no-store`.
get/v1/analytics/publications
Per-publication totals of a workspace's analytics
Parameters:
limit(query),cursor(query),ownerOrgId(query),from(query),to(query),sort(query)Responses: 200, 400, 401, 403, 404, 422, default
Details
Publications with numbers in the range (deleted ones included, `publication: null` once gone), sorted by `sort` then id. Counts only (no player identifiers). Range: inclusive UTC days clamped to the plan's aggregated retention and today; today and days not aggregated yet are computed live. Unique players: consenting players only, exact while raw events are kept; counts of 1–4 are suppressed; never collected for school organizations. Origins are indicative only: events of publications played without an embed token carry the origin the client claims (`embedOrigin`), which nobody verifies; only token embeds record the token's verified origin. API keys with the `analytics:read` scope may call it. `private, no-store`.
get/v1/analytics/publications/{id}
Publication analytics report
Parameters:
id(path, required),from(query),to(query),origin(query)Responses: 200, 400, 401, 403, 404, 422, default
Details
Counts only (no player identifiers). Range: inclusive UTC days clamped to the plan's aggregated retention and today; today and days not aggregated yet are computed live. Unique players: consenting players only, exact while raw events are kept; counts of 1–4 are suppressed; never collected for school organizations. Origins are indicative only: events of publications played without an embed token carry the origin the client claims (`embedOrigin`), which nobody verifies; only token embeds record the token's verified origin. API keys with the `analytics:read` scope may call it. `private, no-store`.
get/v1/analytics/puzzles/{id}
Puzzle analytics report (all its publications)
Parameters:
id(path, required),from(query),to(query),origin(query)Responses: 200, 400, 401, 403, 404, 422, default
Details
Counts only (no player identifiers). Range: inclusive UTC days clamped to the plan's aggregated retention and today; today and days not aggregated yet are computed live. Unique players: consenting players only, exact while raw events are kept; counts of 1–4 are suppressed; never collected for school organizations. Origins are indicative only: events of publications played without an embed token carry the origin the client claims (`embedOrigin`), which nobody verifies; only token embeds record the token's verified origin. API keys with the `analytics:read` scope may call it. `private, no-store`.
get/v1/analytics/series/{id}
Series analytics report (its editions' publications)
Parameters:
id(path, required),from(query),to(query),origin(query)Responses: 200, 400, 401, 403, 404, 422, default
Details
Counts only (no player identifiers). Range: inclusive UTC days clamped to the plan's aggregated retention and today; today and days not aggregated yet are computed live. Unique players: consenting players only, exact while raw events are kept; counts of 1–4 are suppressed; never collected for school organizations. Origins are indicative only: events of publications played without an embed token carry the origin the client claims (`embedOrigin`), which nobody verifies; only token embeds record the token's verified origin. API keys with the `analytics:read` scope may call it. `private, no-store`.
api-keys
get/v1/api-keys
Parameters:
limit(query),cursor(query),ownerOrgId(query)Responses: 200, 400, 401, 403, 404, default
post/v1/api-keys
Create an API key
Responses: 201, 400, 401, 403, 404, 422, default
Details
Returns the key once (`secret`); only its SHA-256 is stored. Use it as `Authorization: Bearer <key>` without cookies.
get/v1/api-keys/{id}
Parameters:
id(path, required)Responses: 200, 401, 403, 404, default
delete/v1/api-keys/{id}
Parameters:
id(path, required)Responses: 204, 401, 403, 404, default
post/v1/api-keys/{id}/rotate
Rotate an API key
Parameters:
id(path, required)Responses: 201, 400, 401, 403, 404, 409, 422, default
Details
Issues a new key with the same name, scopes and expiry (shown once) acting as you; the old key is revoked, or keeps working for `overlapMinutes`.
assets
get/v1/assets
Parameters:
limit(query),cursor(query),ownerOrgId(query),filter[purpose](query)Responses: 200, 400, 401, 403, 404, default
post/v1/assets
Responses: 201, 400, 401, 402, 403, 404, 409, 422, default
get/v1/assets/{id}
Parameters:
id(path, required)Responses: 200, 401, 403, 404, default
delete/v1/assets/{id}
Parameters:
id(path, required),purpose(query)Responses: 204, 401, 403, 404, 409, default
post/v1/uploads
Responses: 201, 400, 401, 403, 404, 409, default
auth
post/v1/auth/confirm-email-change
Responses: 200, 400, 403, default
post/v1/auth/forgot-password
Responses: 202, default
post/v1/auth/login
Responses: 200, 401, 403, 429, default
post/v1/auth/logout
Responses: 204, default
post/v1/auth/register
Responses: 202, 403, 409, 422, default
get/v1/auth/registration
Registration mode
Responses: 200, default
post/v1/auth/reset-password
Responses: 204, 400, 403, default
post/v1/auth/sso/google
Responses: 200, 401, 403, 409, default
post/v1/auth/sso/google/complete
Responses: 201, 400, 403, 409, 422, default
post/v1/auth/token/refresh
Responses: 204, 401, 403, 409, default
post/v1/auth/verify-email
Responses: 200, 400, 401, default
post/v1/auth/verify-email/resend
Responses: 202, default
Billing
post/v1/billing/checkout-sessions
Parameters:
idempotency-key(header, required),Idempotency-Key(header, required)Responses: 201, 400, 401, 403, 409, 422, 503, default
post/v1/billing/portal-sessions
Responses: 201, 401, 403, 404, 503, default
get/v1/billing/subscription
Responses: 200, 401, 403, default
comments
get/v1/comments/{id}
Parameters:
id(path, required)Responses: 200, 400, 404, default
patch/v1/comments/{id}
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, 422, default
delete/v1/comments/{id}
Parameters:
id(path, required)Responses: 204, 400, 401, 403, 404, default
post/v1/comments/{id}/hide
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, default
get/v1/comments/{id}/replies
Parameters:
id(path, required),limit(query),cursor(query)Responses: 200, 400, 404, default
post/v1/comments/{id}/restore
Parameters:
id(path, required)Responses: 200, 401, 403, 404, 409, default
get/v1/publications/{id}/comments
Parameters:
id(path, required),limit(query),cursor(query),sort(query)Responses: 200, 400, 404, default
post/v1/publications/{id}/comments
Parameters:
id(path, required)Responses: 201, 400, 401, 403, 404, 422, 429, default
completions
post/v1/completions
Upload a completion made offline.
Parameters:
Idempotency-Key(header, required)Responses: 200, 201, 400, 401, 403, 404, 409, 410, 422, 429, default
Details
Idempotent per player on `clientCompletionId` (a known one answers 200 with the stored completion; the same id for another publication is 409 `CLIENT_COMPLETION_ID_CONFLICT`). The final state is checked by the engine against the solution of `versionId` (422 `COMPLETION_NOT_SOLVED`); unknown versions or versions of a retired major line are 404 `PUZZLE_VERSION_NOT_FOUND`. Completes the open session named by `playSessionId`, else records a new completed session. Device times must be plausible (422 `COMPLETION_TIME_IMPLAUSIBLE`, checked before the state is compared). When the caller’s payload has no checks (private publications played without `puzzle:read-solution`, students), every upload is a server probe like `POST /v1/play-sessions/:id/check`: refused before the state is compared past the casual check budgets (409 `CHECK_LIMIT_REACHED`, `details { limit, scope: session|daily, retryAfterSeconds? }`: 50 per named session, 100 per player and publication in 24 h, solved or not), throttled to one every 3 s per session (or per player and publication without one) and 30 a minute per client IP (429 `PLAY_RATE_LIMITED`); a wrong state is logged as a probe (`details.checksRemaining` when it named a session). Always unverified (`verificationNote: offline`). Competitive publications and sessions are refused before the state is checked (422 `COMPETITIVE_REQUIRES_SESSION`): complete them online with `POST /v1/play-sessions/:id/complete`. Accepted whatever `serverProgressEnabled` says (FR-PLAY-07); with it off, the session recorded for the completion keeps no game state (`state: null`). New uploads are limited to 10 a minute per player and 3 a minute per player and publication (429 `PLAY_RATE_LIMITED`, `details.retryAfterSeconds`); replays of a known `clientCompletionId` don't count. School students must name one of their own open sessions in `playSessionId` (422 `PLAY_SESSION_REQUIRED` without one; 409 `PLAY_SESSION_ENDED` once it was abandoned: start a new session).
device-auth
post/v1/device-auth/credential
Poll a claimed pairing; get the device credential once approved
Responses: 200, 400, 404, default
post/v1/device-auth/pair
Claim a pairing code on the device
Responses: 201, 400, 404, 429, default
Details
Rate-limited like sign-in; after too many failed claims platform-wide, every claim answers 429 `DEVICE_PAIRING_PAUSED` for a minute. Single use: the code stops working once claimed. Show `confirmationCode` on the device (the admin types it to approve) and poll `POST /v1/device-auth/credential` with the returned token until an admin decides.
post/v1/device-auth/rotate
Replace the device credential
Responses: 200, 400, 401, 403, default
Details
The old credential and its access tokens stop working; store the new one before discarding the old.
post/v1/device-auth/token
Exchange the device credential for a 1-hour access token
Responses: 200, 400, 401, 403, default
Details
Also restarts the offline window (`device.offlineUntil`). Use the token as `Authorization: Bearer <accessToken>` without cookies.
get/v1/device/me
The calling device
Responses: 200, 400, 401, 403, default
get/v1/device/publications
Publications the device may play, newest first
Parameters:
limit(query),cursor(query)Responses: 200, 400, 401, 403, default
Details
The organization's published, standalone-playable publications within the device's scope, private ones included (the device plays them without checks). Load each through its `playPath` with the device's token.
post/v1/device/publications/{id}/check
Check a device’s grid on the server (casual, without checks).
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, 409, 410, 422, 429, default
Details
For publications the device plays without checks (private ones): whether the state solves the version (`solved`), never which entries are wrong; each check is logged as a probe (no state). Publications the device may not play are 404 like the play pointer; taken down 410. Competitive publications need a play session, which devices don’t have (422 `COMPETITIVE_REQUIRES_SESSION`); payloads with checks are checked on the device (422 `CHECKS_AVAILABLE_LOCALLY`). At most 100 checks per device and publication in 24 h, refused before the state is compared (409 `CHECK_LIMIT_REACHED`, `details { limit: 100, scope: daily, retryAfterSeconds }`). Throttled (429 `PLAY_RATE_LIMITED`, `details.retryAfterSeconds`): one check per device and publication every 3 s, 60 per hour, and 30 a minute per client IP.
devices
post/v1/device-pairings/{id}/approve
Approve a claimed pairing
Parameters:
id(path, required)Responses: 200, 400, 401, 402, 403, 404, 409, 422, default
Details
Needs the `confirmationCode` shown on the claiming device. Creates the device (it counts against the plan’s `org_devices_max`), or re-pairs the rotated device the pairing was opened for. The device then picks its credential up.
post/v1/device-pairings/{id}/reject
Reject or cancel a pairing (idempotent)
Parameters:
id(path, required)Responses: 200, 401, 403, 404, 409, default
get/v1/org-devices/{id}
Parameters:
id(path, required)Responses: 200, 401, 403, 404, default
patch/v1/org-devices/{id}
Rename a device, change its scope or offline window
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, 409, 422, default
post/v1/org-devices/{id}/reauthorize
Reauthorize a lapsed device
Parameters:
id(path, required)Responses: 200, 401, 403, 404, 409, default
Details
For a device that stayed offline past `offlineUntil` (status `lapsed`): it may sign in again within a new offline window.
post/v1/org-devices/{id}/revoke
Revoke a device (idempotent)
Parameters:
id(path, required)Responses: 204, 401, 403, 404, default
Details
Its credential and access tokens stop working at once and its plan slot is freed. Content it already cached stays playable until its `offlineUntil` (FR-DEV-ORG-02). `devices:revoke`: also in frozen organizations.
post/v1/org-devices/{id}/rotate
Rotate a device credential
Parameters:
id(path, required)Responses: 200, 401, 403, 404, 409, 422, default
Details
The current credential stops working now; the returned code (shown once) pairs the same device again, keeping its name, scope and plan slot.
get/v1/organizations/{id}/device-pairings
Open pairings (pending, claimed, approved), newest first
Parameters:
id(path, required),limit(query),cursor(query)Responses: 200, 400, 401, 403, 404, default
post/v1/organizations/{id}/device-pairings
Start pairing a device
Parameters:
id(path, required)Responses: 201, 401, 403, 404, 422, default
Details
Returns a single-use code (once) to enter on the device within 10 minutes; then approve the claimed pairing. Never `Idempotency-Key`: the code is not stored.
get/v1/organizations/{id}/devices
The organization's devices, newest first
Parameters:
id(path, required),limit(query),cursor(query),filter[status](query)Responses: 200, 400, 401, 403, 404, default
embeds
get/v1/embed-tokens/jwks
Responses: 200, 503, default
post/v1/publications/{id}/embed-tokens
Parameters:
id(path, required)Responses: 201, 400, 401, 402, 403, 404, 422, 503, default
Entitlements
get/v1/entitlements
Responses: 200, 401, 403, default
games
get/v1/games
Responses: 200, 401, 403, default
Health
get/v1/health
Responses: 200, default
LocalStorage
get/v1/storage/local/objects
Responses: 200, default
get/v1/storage/local/public/{key}
Responses: 200, default
put/v1/storage/local/uploads
Responses: 204, default
moderation
get/v1/moderation/actions
The moderation history, newest first
Parameters:
limit(query),cursor(query),filter[action](query),filter[targetType](query),filter[targetId](query),filter[actor](query),filter[subjectUser](query),filter[subjectOrg](query),filter[report](query)Responses: 200, 400, 401, 403, default
get/v1/moderation/appeals
Appeals, the oldest open first
Parameters:
limit(query),cursor(query),filter[status](query),filter[user](query),sort(query),filter[level](query),filter[kind](query),filter[org](query)Responses: 200, 400, 401, 403, default
Details
`filter[status]` defaults to `open`. Organization-level appeals (first level of organization bans) are their organization’s to resolve; staff may answer one from its `staffResolvableAt` (open 14 days, or the organization disabled or deleted), and that answer is final.
get/v1/moderation/appeals/{appealId}
An appeal with the decision it contests
Parameters:
appealId(path, required)Responses: 200, 401, 403, 404, default
Details
The ban (with its internal reason) or the moderation action, whether it still applies, and the organization-level appeal an escalation follows.
post/v1/moderation/appeals/{appealId}/accept
Accept an appeal and reverse the decision
Parameters:
appealId(path, required)Responses: 200, 400, 401, 403, 404, 409, 422, default
Details
Lifts the ban, restores the content (with the copies its takedown followed) or the comment, or withdraws the warning, through the usual moderation action (with `note` as its reason). Notifies the appellant (`moderation.appeal_accepted`), never naming you. Not on your own decisions, appeals about you or organizations you belong to, or escalations whose first level you answered. Organization-level appeals only from `staffResolvableAt` (else 409 `APPEAL_ORGANIZATION_LEVEL`).
post/v1/moderation/appeals/{appealId}/reject
Reject an appeal
Parameters:
appealId(path, required)Responses: 200, 400, 401, 403, 404, 409, 422, default
Details
The decision stands; the appellant gets `moderation.appeal_rejected` and reads `note` on the appeal. A platform rejection is final.
get/v1/moderation/bans
Every ban, newest first
Parameters:
limit(query),cursor(query),filter[status](query),filter[user](query),filter[scope](query),filter[org](query)Responses: 200, 400, 401, 403, default
get/v1/moderation/bans/{banId}
Parameters:
banId(path, required)Responses: 200, 401, 403, 404, default
post/v1/moderation/bans/{banId}/revoke
Lift a ban early
Parameters:
banId(path, required)Responses: 200, 400, 401, 403, 404, 409, 422, default
Details
Platform and organization bans. Notifies the user (`moderation.ban_revoked`). Refused for bans about yourself or an organization you belong to.
post/v1/moderation/comments/{id}/hide
Hide a comment (platform moderation)
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, 422, default
Details
The author is notified (`comment.hidden`, `source: 'staff'`, never who); the publication's own moderators can't restore it (a workspace hide is taken over). Resolves the open reports on the comment.
post/v1/moderation/comments/{id}/restore
Restore a hidden comment
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, 422, default
Details
Restores hides by staff or by the workspace.
get/v1/moderation/publications/{id}/copies
The puzzle's copies, to offer following them in a takedown
Parameters:
id(path, required)Responses: 200, 400, 401, 403, default
post/v1/moderation/publications/{id}/restore
Restore a taken-down publication
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, 422, default
Details
Restores the puzzle and its publication; its status is kept (a published one is playable again at once). The owner is notified (`moderation.publication_restored`). `includeCopies` restores the copies the puzzle's current takedown followed.
post/v1/moderation/publications/{id}/takedown
Take down a publication
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, 422, default
Details
Takedowns act on the puzzle (`takenDownAt`, also for never-published puzzles) and its live publication: it is no longer played (410 `PUBLICATION_REMOVED`), discussed, rated, embedded, listed, resolved, copied (clone, export, edition source: 409 `PUZZLE_TAKEN_DOWN`) or published (422), until restored. Owners still see it in Studio. The owner is notified (`moderation.publication_taken_down`); open reports on the publication and its puzzle (and on `reportId`'s target) are resolved and their reporters notified. Never on your own content or your organization's (422 `MODERATION_SELF_ACTION`). Edge caches: a purge of the affected URLs is queued; until the owner configures the Cloudflare purge, cached copies expire on their own (pointers, URL resolution, rating summaries and search within 60 s, payloads within 1 h, 5 min in browsers). `includeCopies` also takes down every copy of the puzzle, with or without a publication (each owner notified). Repeating it changes nothing (`changed: false`, no action).
post/v1/moderation/puzzles/{id}/restore
Restore a taken-down puzzle (and its live publication)
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, 422, default
post/v1/moderation/puzzles/{id}/takedown
Take down a puzzle (and its live publication)
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, 422, default
Details
Takedowns act on the puzzle (`takenDownAt`, also for never-published puzzles) and its live publication: it is no longer played (410 `PUBLICATION_REMOVED`), discussed, rated, embedded, listed, resolved, copied (clone, export, edition source: 409 `PUZZLE_TAKEN_DOWN`) or published (422), until restored. Owners still see it in Studio. The owner is notified (`moderation.publication_taken_down`); open reports on the publication and its puzzle (and on `reportId`'s target) are resolved and their reporters notified. Never on your own content or your organization's (422 `MODERATION_SELF_ACTION`). Edge caches: a purge of the affected URLs is queued; until the owner configures the Cloudflare purge, cached copies expire on their own (pointers, URL resolution, rating summaries and search within 60 s, payloads within 1 h, 5 min in browsers). `includeCopies` also takes down every copy of the puzzle, with or without a publication (each owner notified). Repeating it changes nothing (`changed: false`, no action).
get/v1/moderation/queue
The moderation queue: reports grouped by target
Parameters:
limit(query),cursor(query),filter[status](query),filter[targetType](query),filter[reason](query),filter[subjectUser](query),filter[subjectOrg](query),sort(query)Responses: 200, 400, 401, 403, default
get/v1/moderation/reports
Reports, newest first (with their reporters)
Parameters:
limit(query),cursor(query),filter[targetType](query),filter[targetId](query),filter[status](query),filter[reason](query),filter[reporter](query)Responses: 200, 400, 401, 403, default
post/v1/moderation/reports/{id}/dismiss
Dismiss a report (no violation found)
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, 409, 422, default
Details
Like resolve, with status `dismissed`: closes the report and, unless `onlyThisReport`, every open report on its target.
post/v1/moderation/reports/{id}/resolve
Resolve a report without acting on the content
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, 409, 422, default
Details
Closes the report and, unless `onlyThisReport`, every open report on its target. Reporters are notified (`moderation.report_closed`); the reported party never learns who reported.
get/v1/moderation/targets/{targetType}/{targetId}
A reported target: what it is now, its reports and history
Parameters:
targetType(path, required),targetId(path, required)Responses: 200, 400, 401, 403, default
post/v1/moderation/users/{id}/bans
Ban a user (platform-wide or from one feature)
Parameters:
id(path, required)Responses: 201, 400, 401, 403, 404, 409, 422, default
Details
Takes effect at once on every request (`USER_BANNED`). A `platform` ban also signs the user out everywhere; they can sign in again but only read their bans (`GET /v1/users/me/bans`), exercise their data rights (exports, account deletion, consent) and manage their notifications. Notifies the user (`moderation.banned`: scope, violation, expiry; never who decided or reported) and resolves the open reports on the user (and on `reportId`'s target).
post/v1/moderation/users/{id}/warn
Warn a user
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, 422, default
Details
Notifies the user (`moderation.warning` with the `violation`, never who decided or who reported). Resolves the open reports on the user (and on `reportId`'s target).
notifications
get/v1/notifications
List your notifications, newest first.
Parameters:
limit(query),cursor(query),filter[unread](query)Responses: 200, 400, 401, 403, default
Details
Types and their payloads: - `comment.reply` (community): Someone replied to your comment. Payload: `publicationId`, `commentId` (the reply), `parentCommentId`, `puzzleTitle`. - `comment.hidden` (moderation): A moderator of the publication (or platform staff) hid your comment. Payload: `publicationId`, `commentId`, `puzzleTitle`, `source` (`workspace`|`staff`). - `moderation.publication_taken_down` (moderation): Platform moderation took down a puzzle of yours (or of an organization you own) and its publication: it is no longer played, discussed, copied, published or listed. Payload: `publicationId` (null for puzzles without a publication), `puzzleId`, `puzzleTitle`, `violation` (a report reason, or null), `followedCopy` (true when it was taken down as a copy of other taken-down content), `ownerOrgId` (null for personal content). - `moderation.publication_restored` (moderation): Platform moderation restored a puzzle of yours (and its publication) that was taken down. Payload: `publicationId` (or null), `puzzleId`, `puzzleTitle`, `ownerOrgId`. - `moderation.warning` (moderation): Platform moderation warned you about your behaviour. Payload: `actionId`, `violation` (a report reason). - `moderation.banned` (moderation): You were banned (E13-T02): by the platform's moderators (`scope` `platform`: everything but reading your bans, your data rights and your notifications; `comments`, `ratings` or `publishing`: that feature everywhere) or by an organization (`scope` `org`: its workspace, membership and content). Never says who decided or who reported. Payload: `banId`, `scope`, `orgId` and `orgName` (org bans, else null), `violation` (a report reason), `expiresAt` (ISO date-time, null = permanent). - `moderation.ban_revoked` (moderation): A ban of yours was lifted before it expired. Payload: `banId`, `scope`, `orgId`, `orgName`. - `moderation.ban_expired` (moderation): A temporary ban of yours ended. Payload: `banId`, `scope`, `orgId`, `orgName`. - `moderation.appeal_submitted` (moderation): Your appeal was received (E13-T03). Payload: `appealId`, `kind` (`ban`|`takedown`|`comment_hide`|`warning`), `level` (`organization`: the organization that banned you reviews it first; `platform`: platform moderation), `banId` or `actionId` (the decision; the other is null), `orgId` and `orgName` (organization bans and organization content, else null). - `moderation.appeal_received` (moderation): Sent to an organization's owner: someone the organization banned appealed the ban, and the organization's admins (`organization:bans:manage`) review it first (E13-T03). Never the appellant's text. Payload: `appealId`, `banId`, `orgId`, `orgName`. - `moderation.appeal_accepted` (moderation): Your appeal was accepted and the decision reversed (the ban lifted, the content restored, the comment shown again; a warning is withdrawn). Never says who reviewed it; the answer is on the appeal (`GET /v1/users/me/appeals/:id`). Payload: `appealId`, `kind`, `level`, `banId`, `actionId`, `orgId`, `orgName`, `reversed` (false when the decision had already ended). - `moderation.appeal_rejected` (moderation): Your appeal was rejected; the decision stands. Never says who reviewed it; the answer is on the appeal. Payload: `appealId`, `kind`, `level`, `banId`, `actionId`, `orgId`, `orgName`, `escalatableUntil` (organization-level rejections: until when platform moderation can be asked to review it, ISO date-time; else null). - `moderation.report_closed` (moderation): A report you filed was closed. Never says what was done to whom. Payload: `reportId`, `targetType`, `status` (`resolved`|`dismissed`). - `organization.invitation` (organizations): You were invited to an organization (your verified address). Payload: `orgId`, `orgName`, `invitationId`. - `organization.ownership_transfer_offered` (organizations): An organization's owner offered you its ownership. Payload: `orgId`, `orgName`, `transferId`. - `organization.frozen` (organizations): An organization you belong to was fr
post/v1/notifications/{id}/read
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, default
get/v1/notifications/preferences
Responses: 200, 401, 403, default
patch/v1/notifications/preferences
Responses: 200, 400, 401, 403, default
post/v1/notifications/read-all
Responses: 200, 401, 403, default
get/v1/notifications/unread-count
Responses: 200, 401, 403, default
post/v1/notifications/unsubscribe
Responses: 200, 400, default
OpenApi
get/v1/openapi.json
Responses: 503, default
organizations
post/v1/invitations/accept
Responses: 200, 400, 401, 402, 403, 404, 409, 422, default
post/v1/invitations/preview
Responses: 200, 400, 401, 403, 404, 422, default
post/v1/invitations/reject
Responses: 204, 400, 401, 403, 404, 422, default
get/v1/organizations
Parameters:
limit(query),cursor(query)Responses: 200, 400, 401, 403, default
post/v1/organizations
Responses: 201, 400, 401, 402, 403, 409, default
get/v1/organizations/{id}
Parameters:
id(path, required)Responses: 200, 401, 403, 404, default
patch/v1/organizations/{id}
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, default
get/v1/organizations/{id}/appeals
Appeals of the organization's bans, the oldest open first
Parameters:
id(path, required),limit(query),cursor(query),filter[status](query),filter[user](query),sort(query)Responses: 200, 400, 401, 403, 404, default
get/v1/organizations/{id}/appeals/{appealId}
Parameters:
id(path, required),appealId(path, required)Responses: 200, 401, 403, 404, default
post/v1/organizations/{id}/appeals/{appealId}/accept
Accept an appeal and lift the ban
Parameters:
id(path, required),appealId(path, required)Responses: 200, 400, 401, 403, 404, 409, 422, default
Details
Audited `organization.appeal_accepted` and `organization.ban_revoked` (with `note` as the revoke reason of the ban; audit entries keep only its length). Notifies the appellant (`moderation.appeal_accepted`), never naming you.
post/v1/organizations/{id}/appeals/{appealId}/reject
Reject an appeal
Parameters:
id(path, required),appealId(path, required)Responses: 200, 400, 401, 403, 404, 409, 422, default
Details
The ban stands. The appellant gets `moderation.appeal_rejected` and may ask platform moderation within 30 days.
get/v1/organizations/{id}/bans
The organization's bans, newest first
Parameters:
id(path, required),limit(query),cursor(query),filter[status](query),filter[user](query)Responses: 200, 400, 401, 403, 404, default
post/v1/organizations/{id}/bans
Ban a user from the organization
Parameters:
id(path, required)Responses: 201, 400, 401, 403, 404, 409, 422, 429, default
Details
Takes effect at once: the user can't act in the organization (a member keeps the membership, but every permission there answers `USER_BANNED`), be invited or join, or comment on, rate or open its content. Only current members, pending invitees and users who commented on, rated or reported the organization's content in the last 90 days (anyone else, unknown users included: 404 `BAN_TARGET_NOT_FOUND`). Never yourself, platform staff or the owner; banning a member needs every permission of their role. At most 20 bans per hour and 3 of the same user per day (429 `BAN_RATE_LIMITED`, `details.retryAfterSeconds`). Notifies the user (`moderation.banned` naming the organization, never who decided; mailed to members only, in-app for others).
get/v1/organizations/{id}/bans/{banId}
Parameters:
id(path, required),banId(path, required)Responses: 200, 401, 403, 404, default
post/v1/organizations/{id}/bans/{banId}/revoke
Lift an organization ban early
Parameters:
id(path, required),banId(path, required)Responses: 200, 400, 401, 403, 404, 409, default
Details
Notifies the user (`moderation.ban_revoked`).
post/v1/organizations/{id}/claim
Parameters:
id(path, required)Responses: 200, 401, 402, 403, 404, 409, 422, default
get/v1/organizations/{id}/dpa-acceptances
The organization's DPA acceptances, newest first
Parameters:
id(path, required),limit(query),cursor(query)Responses: 200, 400, 401, 403, 404, default
post/v1/organizations/{id}/dpa-acceptances
Accept the current DPA version for a school
Parameters:
id(path, required)Responses: 201, 400, 401, 403, 404, 409, 422, default
Details
For schools whose accepted version is no longer current.
get/v1/organizations/{id}/invitations
Parameters:
id(path, required),limit(query),cursor(query)Responses: 200, 400, 401, 403, 404, default
post/v1/organizations/{id}/invitations
Parameters:
id(path, required)Responses: 201, 400, 401, 402, 403, 404, 409, 422, 429, default
delete/v1/organizations/{id}/invitations/{invitationId}
Parameters:
id(path, required),invitationId(path, required)Responses: 204, 400, 401, 403, 404, default
post/v1/organizations/{id}/leave
Parameters:
id(path, required)Responses: 204, 401, 403, 404, 422, default
get/v1/organizations/{id}/members
Parameters:
id(path, required),limit(query),cursor(query)Responses: 200, 400, 401, 403, 404, default
patch/v1/organizations/{id}/members/{userId}
Parameters:
id(path, required),userId(path, required)Responses: 200, 400, 401, 403, 404, 422, default
delete/v1/organizations/{id}/members/{userId}
Parameters:
id(path, required),userId(path, required)Responses: 204, 400, 401, 403, 404, 422, default
get/v1/organizations/{id}/ownership-transfer
Parameters:
id(path, required)Responses: 200, 401, 403, 404, default
post/v1/organizations/{id}/ownership-transfer
Parameters:
id(path, required)Responses: 201, 400, 401, 403, 404, 422, default
delete/v1/organizations/{id}/ownership-transfer
Parameters:
id(path, required)Responses: 204, 401, 403, 404, default
post/v1/organizations/{id}/ownership-transfer/accept
Parameters:
id(path, required)Responses: 200, 400, 401, 402, 403, 404, 422, default
post/v1/organizations/{id}/ownership-transfer/decline
Parameters:
id(path, required)Responses: 204, 400, 401, 403, 404, default
get/v1/organizations/{id}/roles
Parameters:
id(path, required)Responses: 200, 401, 403, 404, default
post/v1/organizations/{id}/roles
Parameters:
id(path, required)Responses: 201, 400, 401, 403, 404, 409, 422, default
patch/v1/organizations/{id}/roles/{roleId}
Parameters:
id(path, required),roleId(path, required)Responses: 200, 400, 401, 403, 404, 409, 422, default
delete/v1/organizations/{id}/roles/{roleId}
Parameters:
id(path, required),roleId(path, required),reassignTo(query)Responses: 204, 400, 401, 403, 404, 409, 422, default
post/v1/organizations/{id}/school-setup
Turn the organization into a school
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, 409, 422, default
Details
Accepts the current DPA for the school and changes the organization type to `school` (school mode: comments off). Schools stay schools.
get/v1/organizations/{id}/settings
Parameters:
id(path, required)Responses: 200, 401, 403, 404, default
patch/v1/organizations/{id}/settings
Parameters:
id(path, required),If-Match(header, required)Responses: 200, 400, 401, 403, 404, 412, 428, default
get/v1/organizations/{id}/usage
Parameters:
id(path, required)Responses: 200, 401, 403, 404, default
get/v1/school-dpa
The data processing agreement schools accept now
Responses: 200, default
Details
Send `version` back in `dpa.version` when creating a school or accepting the DPA.
Permissions
get/v1/permissions
Responses: 200, default
play
get/v1/publications/{id}/play
Play pointer of a publication
Parameters:
id(path, required),X-Embed-Parent-Origin(header),X-Embed-Token(header)Responses: 200, 401, 403, 404, 410, default
Details
The current version, runtime settings and embed data (`embed.frameAncestors` for the play document's CSP). Caching: public and unlisted responses without a token are shared-cacheable. After the publication switches to private, starts requiring embed tokens, is unpublished or is taken down, copies cached before the change can still be served by the edge until they expire: up to 60 s for the pointer and up to 1 h for payloads (5 min in browsers), until the instant purge of E13-T02. Responses with a token, and responses for private or token-required publications, are `private, no-store`. A personal owner who is suspended or deleted stops serving their publications: 404 like an unknown publication (`no-store`), with an edge purge queued on the change; restoring the account serves them again. Organization state never matters here.
post/v1/publications/{id}/reveal
Reveal the answers of some positions (casual play only).
Parameters:
id(path, required),X-Embed-Parent-Origin(header),X-Embed-Token(header)Responses: 200, 400, 401, 403, 404, 409, 410, 422, 429, default
Details
Returns only the requested positions’ answers (a cell or an entry, never every answer at once) and, for cryptic crosswords, the annotations of clues this request revealed completely. Online only. Refused with 403 `REVEAL_DISABLED` (`details.reason`: `competitive`, or `disabled` when the pointer’s `settings.reveal.enabled` is false). With `playSessionId` the signed-in player’s open casual session counts one reveal and becomes `assisted` (FR-PLAY-06). Every reveal by a signed-in player is also logged for the publication (with or without a session, kept when progress is deleted), and such players never appear on its casual leaderboards.
get/v1/versions/{versionId}/payload
Version payload for play
Parameters:
versionId(path, required),mode(query),X-Embed-Parent-Origin(header),X-Embed-Token(header)Responses: 200, 400, 401, 403, 404, 409, 410, default
Details
The public definition and, for casual play with access to them, the checks; never the solution. Caching: public and unlisted responses without a token are shared-cacheable. After the publication switches to private, starts requiring embed tokens, is unpublished or is taken down, copies cached before the change can still be served by the edge until they expire: up to 60 s for the pointer and up to 1 h for payloads (5 min in browsers), until the instant purge of E13-T02. Responses with a token, and responses for private or token-required publications, are `private, no-store`. A personal owner who is suspended or deleted stops serving their publications: 404 like an unknown publication (`no-store`), with an edge purge queued on the change; restoring the account serves them again. Organization state never matters here.
play-sessions
post/v1/play-sessions
Start a play session of a publication.
Parameters:
Idempotency-Key(header, required)Responses: 201, 400, 401, 403, 404, 409, 410, 422, default
Details
Pins the publication’s current version and mode and starts from the engine’s initial state. Casual sessions need `serverProgressEnabled` in the player’s settings (422 `SERVER_PROGRESS_DISABLED`: casual play then stays on the device). Private publications need `puzzle:read` (404 otherwise); taken-down ones are 410. Multiple attempts are allowed, at most 10 open (active or paused) per publication and mode. School students (`Authorization: Bearer ppst_…`, no cookies) start sessions on their class’s assignments only (anything else is 404; competitive publications only when the teacher assigned them as competitive); their progress is always kept on the server.
get/v1/play-sessions/{id}
Read one of your play sessions.
Parameters:
id(path, required)Responses: 200, 401, 403, 404, default
Details
The `ETag` is the session’s `revision` (the `If-Match` / `baseRevision` of the next progress sync).
post/v1/play-sessions/{id}/abandon
Parameters:
id(path, required)Responses: 200, 401, 403, 404, 409, 422, default
post/v1/play-sessions/{id}/check
Check a session’s grid on the server (competitive, or casual without checks).
Parameters:
id(path, required)Responses: 200, 401, 403, 404, 409, 410, 422, 429, default
Details
Answers only whether the state solves the puzzle (`solved`), never which entries are wrong. **Competitive** (payloads carry no checks): each check uses one of the player’s 3 on the publication, shared by all their competitive sessions of it and kept when sessions are deleted (`checksRemaining`; failed completions use them too); then 409 `CHECK_LIMIT_REACHED` on every session of the publication. Probing another session also makes this one a repeat attempt (never verified). The state must be for the session’s `versionId` (409 `PLAY_SESSION_VERSION_MISMATCH`). Throttled like completions (429 `PLAY_RATE_LIMITED`). **Casual**: only when the caller’s payload has no checks (`protection: null`: private publications played by grantees, school students or members without `puzzle:read-solution`); with checks the runtime checks locally (422 `CHECKS_AVAILABLE_LOCALLY`, nothing counted). Bounded before the state is evaluated (no refusal depends on the answer): 50 per session (`checksRemaining`; failed completions of such sessions count too, never ending it), then 409 `CHECK_LIMIT_REACHED` with `details { limit: 50, scope: session }` (start a new session); 100 per player and publication over the casual sessions started in the last 24 h, then 409 `CHECK_LIMIT_REACHED` with `details { limit: 100, scope: daily, retryAfterSeconds }`; one per session every 3 s, 10 a minute per player and session, and 30 a minute per client IP (429 `PLAY_RATE_LIMITED`, `details.retryAfterSeconds`). When `solved` is true, complete the session with `POST …/complete` (the server checks the state again). The state defaults to the session’s `versionId`; casual sessions may send a newer minor.
post/v1/play-sessions/{id}/complete
Complete an open play session with its final state.
Parameters:
id(path, required),Idempotency-Key(header, required)Responses: 200, 400, 401, 403, 404, 409, 410, 422, 429, default
Details
The server checks the state with the engine against the stored solution (422 `COMPLETION_NOT_SOLVED`: nothing recorded). Casual completions are never verified (`verificationNote: casual`) and a wrong casual state leaves the session open; when the caller’s payload has no checks, every attempt is a server probe: refused before the state is evaluated past the casual check budgets (409 `CHECK_LIMIT_REACHED`, `details { limit, scope: session|daily, retryAfterSeconds? }`, solved or not) and throttled to one per session every 3 s and 30 a minute per client IP (429 `PLAY_RATE_LIMITED`); a wrong state counts one check (`details.checksRemaining`, `sessionEnded: false`). Competitive ones use the session’s `versionId` (409 `PLAY_SESSION_VERSION_MISMATCH` for another), are timed by the server (start of the session to now, no pauses) and verified unless the publication changed mode, answers were revealed, it is not the player’s first attempt at the publication (`repeat_attempt`: any other competitive session of theirs on it, earlier or later, that was completed, saved progress, or was checked or failed a completion, also once deleted), more than 24 h passed or the time is below the game’s plausible minimum (`verificationNote`). A wrong competitive state uses one of the player’s 3 checks on the publication, shared by all their sessions of it (`details.checksRemaining`); with none left the session is abandoned (`details.sessionEnded`, `endedReason: too_many_attempts`). At most 10 checks and completions per minute per player and session (429 `PLAY_RATE_LIMITED`, `details.retryAfterSeconds`). A completed session answers its completion again. With `serverProgressEnabled` off a casual completion is still recorded, but the session keeps no final state (`state: null`).
post/v1/play-sessions/{id}/pause
Parameters:
id(path, required)Responses: 200, 401, 403, 404, 409, 422, default
put/v1/play-sessions/{id}/progress
Save the progress of an open play session.
Parameters:
id(path, required),If-Match(header),Idempotency-Key(header, required)Responses: 200, 400, 401, 403, 404, 409, 410, 412, 422, 428, default
Details
Optimistic concurrency on the session’s `revision` (`baseRevision` or `If-Match`; one is required, 428): a stale base is 409 `PLAY_SESSION_REVISION_CONFLICT` with the server copy in `details.session` and `details.revision`; let the player choose a copy and resend with `baseRevision = details.revision`. Re-sending the last applied `clientMutationId` returns the session unchanged. The state is validated by the engine (`PLAY_STATE_INVALID`, `PLAY_STATE_MAJOR_UNSUPPORTED`). When the publication moved to a newer minor version the server migrates the state (`migrated: true`, new `versionId`). `revealsUsed`, `hintsUsed` and `assisted` never go down. Competitive states must be for the session’s `versionId` (409 `PLAY_SESSION_VERSION_MISMATCH`; only the server migrates them). Casual sessions need `serverProgressEnabled`. The response `ETag` is the new revision.
post/v1/play-sessions/{id}/resume
Parameters:
id(path, required)Responses: 200, 401, 403, 404, 409, 422, default
players
get/v1/players/me/achievements
Your achievements.
Responses: 200, 400, 401, 403, default
Details
Every achievement of the catalog (E12-T06) with your progress, computed from your statistics (streak days in UTC), whether or not your profile is public. Others see the achieved ones on your public profile (`GET /v1/profiles/:handle`) while `profilePublic` is on.
get/v1/players/me/completions
List your completions.
Parameters:
limit(query),cursor(query),filter[gameType](query),filter[mode](query),filter[verified](query),filter[publicationId](query),sort(query)Responses: 200, 400, 401, 403, default
Details
Every completion you made (casual, offline and competitive; unverified ones included, FR-PLAY-07), newest first by default, filterable by game type, mode, verification and publication. Never a state or a solution.
delete/v1/players/me/progress
Delete your server progress.
Responses: 200, 401, 403, default
Details
Deletes every unfinished casual session (active, paused, abandoned) with its stored game state (FR-PRIV-02; e.g. after turning `serverProgressEnabled` off). Completed sessions and completions stay (your history), and so do competitive attempts (server-timed and verified). Idempotent. Allowed while banned (it only removes your data); refused while impersonating. Turning `serverProgressEnabled` off does the same automatically.
get/v1/players/me/sessions
List your play sessions (saved games and history).
Parameters:
limit(query),cursor(query),publicationIds(query),filter[status](query),filter[mode](query),sort(query)Responses: 200, 400, 401, 403, default
Details
Newest first by default. `filter[status]=open` lists saved games to resume (active or paused). `publicationIds` (comma-separated, at most 100) answers the offline sync prompt (architecture §12.3): the server copies of the device’s local games, to compare `revision`/`updatedAt` with the device copies. Entries carry no engine state: read `GET /v1/play-sessions/:id` for it. Sessions of other players never appear.
get/v1/players/me/statistics
Your play statistics.
Parameters:
timeZone(query)Responses: 200, 400, 401, 403, default
Details
Session counts, completion counts, daily streaks and, per game type, completions, average and best times (without revealed answers) and the best verified time. Streak days are calendar days in `timeZone` (IANA name, default UTC); a streak stays current until a whole day passes without a completion.
privacy
get/v1/organizations/{id}/deletion
The organization's latest deletion request
Parameters:
id(path, required)Responses: 200, 401, 403, 404, default
post/v1/organizations/{id}/deletion
Schedule the deletion of the organization
Parameters:
id(path, required)Responses: 202, 400, 401, 403, 404, 409, 422, 429, default
Details
Owner only. Freezes the organization now (reason `deletion_requested`: content becomes read-only, members are notified, nobody can claim it; published puzzles keep serving) and soft-deletes it at the freeze deadline, 14 days later (an earlier deadline of a freeze for a lost plan is kept), unless the owner cancels. Its members, invitations and unpublished content are purged `retention.deletedOrganizationsDays` (90) after the deletion; published puzzles stay while published. Needs a sign-in within the last 5 minutes.
delete/v1/organizations/{id}/deletion
Cancel the scheduled deletion
Parameters:
id(path, required)Responses: 200, 401, 403, 404, 409, default
Details
Owner only, before the deadline, with a sign-in within the last 5 minutes. The organization is active again when the owner holds the Organization Owner plan; otherwise it stays frozen for the lost plan with the same deadline (members holding the plan can then claim it).
get/v1/organizations/{id}/exports
Parameters:
id(path, required),limit(query),cursor(query)Responses: 200, 400, 401, 403, 404, default
post/v1/organizations/{id}/exports
Request an export of the organization's data
Parameters:
id(path, required)Responses: 202, 401, 403, 404, 409, 429, default
get/v1/organizations/{id}/exports/{exportId}
Parameters:
id(path, required),exportId(path, required)Responses: 200, 401, 403, 404, default
post/v1/organizations/{id}/exports/{exportId}/download
Get a download link for a ready organization export
Parameters:
id(path, required),exportId(path, required)Responses: 200, 401, 403, 404, 409, 410, default
Details
A signed URL valid for 5 minutes.
post/v1/privacy/account-deletion
Delete your account
Responses: 202, 400, 401, 403, 422, 429, default
Details
Soft-deletes the account now (every session is signed out, owned organizations are frozen for their members to claim, API keys are revoked, webhooks disabled, subscriptions canceled) and purges its personal data after `retention.deletedAccountsDays` (30 days by default): personal puzzles are deleted, organization content stays with the organization, comments stay shown as a deleted user, completed play stays anonymised. Needs a sign-in within the last 5 minutes. There is no undo.
get/v1/privacy/consents
Read your consent and its history
Parameters:
anonymousId(query)Responses: 200, 400, 401, 403, default
Details
Signed in: the account's records. Signed out: those of `anonymousId` (400 `ANONYMOUS_ID_REQUIRED` without it). `current: null` means no choice yet: optional categories are off.
put/v1/privacy/consents
Record a consent choice (grant or withdraw)
Responses: 200, 400, 401, 403, 429, default
Details
Records a new version when the choice differs from the current one (the same choice again only confirms it). Withdrawal is a choice with `analytics: false`; it takes effect for the next analytics batch. The player's choice wins: an `embed_sdk` choice made while the current record is a `banner` or `settings` withdrawal is stored in the history but never becomes `current` (analytics stay off until the player grants through the banner or settings). `anonymousId` is a device secret: whoever knows it can read and change that device's consent, so keep it on the device and never put it in URLs that are shared or logged. Anonymous choices lapse when not confirmed for `retention.consentRecordsDays` (3 years). At most 20 new anonymous ids per IP per hour (429 `CONSENT_RATE_LIMITED`).
get/v1/privacy/exports
Parameters:
limit(query),cursor(query)Responses: 200, 400, 401, 403, default
post/v1/privacy/exports
Request an export of your personal data
Responses: 202, 401, 403, 409, 429, default
Details
Queues the archive (profile, settings, sessions, devices, sign-in methods, comments, support tickets without internal notes, consents, memberships and your puzzles' metadata; never other people's data). One at a time (409 `EXPORT_IN_PROGRESS`), one per 24 hours (429 `EXPORT_RATE_LIMITED`, `details.retryAfterSeconds`; failed ones don't count). You get an email when it is ready.
get/v1/privacy/exports/{id}
Parameters:
id(path, required)Responses: 200, 401, 403, 404, default
post/v1/privacy/exports/{id}/download
Get a download link for a ready export
Parameters:
id(path, required)Responses: 200, 401, 403, 404, 409, 410, default
Details
A signed URL valid for 5 minutes (ask again for a new one). 409 `EXPORT_NOT_READY` while pending, processing or failed; 410 `EXPORT_EXPIRED` once deleted.
profiles
get/v1/profiles/{handle}
A public profile
Parameters:
handle(path, required)Responses: 200, 400, 404, 429, default
Details
`handle` is a username or an organization slug (a leading `@` is accepted). No query parameters (any is 400). At most 20 requests a minute per IP. Users: display name, avatar, public statistics, achieved achievements, best-players positions and newest public puzzles, only while their profile is public. Organizations (never schools, and only with at least one public puzzle): name, brand image and newest public puzzles. No emails or account data. Private, unknown, deleted and suspended handles are one 404.
get/v1/profiles/{handle}/puzzles
A public profile's puzzles
Parameters:
handle(path, required),limit(query),cursor(query),filter[gameType](query),sort(query)Responses: 200, 400, 404, 429, default
Details
Discoverable puzzles (public, published, not taken down, playable standalone) of the user or organization, newest first by default; listing cards only, never content. Unknown query parameters are 400; at most 20 requests a minute per IP.
publications
delete/v1/access-grants/{id}
Parameters:
id(path, required)Responses: 204, 401, 403, 404, default
get/v1/public-urls/resolve
Parameters:
path(query, required)Responses: 200, 400, 404, 410, default
get/v1/publications
Parameters:
limit(query),cursor(query),ownerOrgId(query),filter[status](query),filter[visibility](query),sort(query)Responses: 200, 400, 401, 403, 404, default
get/v1/publications/{id}
Parameters:
id(path, required)Responses: 200, 401, 403, 404, default
patch/v1/publications/{id}
Parameters:
id(path, required),If-Match(header, required)Responses: 200, 400, 401, 403, 404, 409, 412, 422, 428, default
get/v1/publications/{id}/access-grants
Parameters:
id(path, required),limit(query),cursor(query)Responses: 200, 400, 401, 403, 404, default
post/v1/publications/{id}/access-grants
Parameters:
id(path, required)Responses: 201, 400, 401, 403, 404, 409, 422, default
post/v1/publications/{id}/archive
Parameters:
id(path, required)Responses: 200, 401, 403, 404, default
post/v1/publications/{id}/publish
Parameters:
id(path, required)Responses: 200, 401, 402, 403, 404, 409, 422, default
post/v1/publications/{id}/restore
Parameters:
id(path, required)Responses: 200, 401, 403, 404, 409, default
post/v1/publications/{id}/unpublish
Parameters:
id(path, required)Responses: 200, 401, 403, 404, 409, default
get/v1/puzzles/{id}/publications
Parameters:
id(path, required)Responses: 200, 401, 403, 404, default
post/v1/puzzles/{id}/publications
Parameters:
id(path, required)Responses: 201, 400, 401, 403, 404, 409, 422, default
puzzles
get/v1/puzzles
Parameters:
limit(query),cursor(query),ownerOrgId(query),filter[gameType](query),filter[language](query),filter[difficulty](query),filter[tag](query),filter[q](query),sort(query)Responses: 200, 400, 401, 403, 404, default
post/v1/puzzles
Responses: 201, 400, 401, 402, 403, 404, 422, default
get/v1/puzzles/{id}
Parameters:
id(path, required)Responses: 200, 401, 403, 404, default
patch/v1/puzzles/{id}
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, 422, default
delete/v1/puzzles/{id}
Parameters:
id(path, required)Responses: 204, 401, 403, 404, default
post/v1/puzzles/{id}/clone
Parameters:
id(path, required)Responses: 201, 400, 401, 402, 403, 404, 409, 422, default
get/v1/puzzles/{id}/draft
Parameters:
id(path, required)Responses: 200, 401, 403, 404, default
put/v1/puzzles/{id}/draft
Parameters:
id(path, required),If-Match(header, required)Responses: 200, 400, 401, 403, 404, 412, 413, 422, 428, default
post/v1/puzzles/{id}/import
Parameters:
id(path, required),format(query, required),If-Match(header, required),Idempotency-Key(header, required)Responses: 200, 400, 401, 403, 404, 409, 412, 413, 415, 422, 428, default
post/v1/puzzles/{id}/validate
Parameters:
id(path, required)Responses: 200, 401, 403, 404, 422, default
get/v1/puzzles/{id}/versions
Parameters:
id(path, required),limit(query),cursor(query)Responses: 200, 400, 401, 403, 404, default
post/v1/puzzles/{id}/versions
Parameters:
id(path, required)Responses: 201, 400, 401, 403, 404, 409, 422, default
get/v1/puzzles/{id}/versions/{versionId}
Parameters:
id(path, required),versionId(path, required),include(query)Responses: 200, 401, 403, 404, default
get/v1/puzzles/{id}/versions/{versionId}/export
Parameters:
id(path, required),versionId(path, required),format(query, required)Responses: 200, 400, 401, 403, 404, 409, 422, default
post/v1/puzzles/import
Parameters:
format(query, required),ownerOrgId(query),gameType(query, required),title(query),language(query),Idempotency-Key(header, required)Responses: 201, 400, 401, 402, 403, 404, 409, 413, 415, 422, default
rankings
get/v1/rankings/me
Your place on a leaderboard
Parameters:
window(query),periodStart(query),board(query, required)Responses: 200, 400, 401, 403, 404, default
Details
Your row on one board period (`board` = a board key), with the board’s own rules: the board must be one you may see (token-only embeds need `X-Embed-Token`), and `entry` is null when you aren’t in the stored top 100 or wouldn’t be shown (leaderboard visibility off, banned). Signed-in users only; students, devices and API keys are refused.
get/v1/rankings/players
Best players (casual, unverified)
Parameters:
window(query),periodStart(query),limit(query),filter[gameType](query)Responses: 200, 400, 404, default
Details
Casual results: client-timed, **unverified** (label them so): online casual completions without hints, reveals or assists, a player's best per publication. Best players of discoverable publications (never schools’), per game type (`filter[gameType]`) or every type, by results counted, then the game’s scoring over the sums. Entries show username, display name and avatar only. Public and the same for every caller: `public, max-age=0, s-maxage=60`; boards are rebuilt hourly.
get/v1/rankings/players/competitive
Best players (competitive, verified)
Parameters:
window(query),periodStart(query),limit(query),filter[gameType](query)Responses: 200, 400, 404, default
Details
Verified competitive results only: each player's server-timed first attempt per publication. Best players of discoverable publications (never schools’), per game type (`filter[gameType]`) or every type, by results counted, then the game’s scoring over the sums. Entries show username, display name and avatar only. Public and the same for every caller: `public, max-age=0, s-maxage=60`; boards are rebuilt hourly.
get/v1/rankings/publications/{id}
A publication's leaderboard
Parameters:
id(path, required),window(query),periodStart(query),limit(query),kind(query)Responses: 200, 400, 404, default
Details
Only for live, published, public or unlisted publications that aren’t taken down or owned by a school; private ones never have a public board. Embed-only publications that require tokens need their `X-Embed-Token` (then `private, no-store`); otherwise `public, max-age=0, s-maxage=60` (`Vary: x-embed-token`). Default kind: the publication’s play mode. Everything else is 404 `LEADERBOARD_NOT_FOUND`.
get/v1/rankings/puzzles
Landing rankings of public puzzles
Parameters:
list(query, required),window(query),filter[gameType](query),limit(query)Responses: 200, 400, default
Details
Most played, trending, top rated and most commented discoverable puzzles (public, published, not taken down, not a school’s), with their listing card (no content). Recomputed hourly; `public, max-age=0, s-maxage=60`.
get/v1/rankings/series/{id}
A series' leaderboard
Parameters:
id(path, required),window(query),periodStart(query),limit(query),kind(query)Responses: 200, 400, 404, default
Details
Results on the series’ public and unlisted editions, by results counted, with the series title (`seriesTitle`). Only for series whose owner is live (an active user, or an organization neither disabled, deleted nor a school) with at least one public, playable edition; else 404 `LEADERBOARD_NOT_FOUND`. Default kind `competitive`. `public, max-age=0, s-maxage=60`.
ratings
post/v1/publications/{id}/ratings
Rate a publication (or change your rating)
Parameters:
id(path, required)Responses: 200, 201, 400, 401, 403, 404, 422, 429, default
Details
One rating per player and publication: 201 when created, 200 when changed or repeated. Only after starting the publication (422 `RATING_REQUIRES_PLAY`), never for your own workspace (422 `RATING_OWN_CONTENT`). At most 10 rating writes (sets, repeats and removals) a minute and 60 an hour (429 `RATING_RATE_LIMITED`).
get/v1/publications/{id}/ratings/me
Your rating of a publication
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, default
delete/v1/publications/{id}/ratings/me
Remove your rating
Parameters:
id(path, required)Responses: 204, 400, 401, 403, 429, default
Details
Idempotent: 204 whether or not you had rated it. Counts against the rating write limit (429 `RATING_RATE_LIMITED`).
get/v1/publications/{id}/ratings/summary
A publication's rating summary
Parameters:
id(path, required)Responses: 200, 400, 404, default
Details
Count, plain mean, Bayesian `weightedAverage` and distribution. Cached at the edge for 60 s (`public, s-maxage=60`) unless an `X-Embed-Token` is sent (embed-only publications that require tokens need one; then `private, no-store`).
reports
post/v1/reports
Report content or an account
Responses: 200, 201, 400, 401, 403, 404, 422, 429, default
Details
Reports only what you can see (404 `REPORT_TARGET_NOT_FOUND` otherwise, whatever the type). One open report per target: repeating it answers 200 with the existing report (`created: false`). At most 5 reports a minute, 20 an hour and 50 a day (429 `REPORT_RATE_LIMITED`). The reported party never learns who reported them.
schools
get/v1/classes/{id}
A class (never its join code)
Parameters:
id(path, required)Responses: 200, 401, 403, 404, default
patch/v1/classes/{id}
Rename a class
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, default
get/v1/classes/{id}/analytics
The class's analytics
Parameters:
id(path, required),from(query),to(query)Responses: 200, 400, 401, 403, 404, 422, default
Details
Totals, per-assignment completion and medians, and sessions and completions per UTC day (E20-T04), counted over the class's current, unrevoked students and current assignments only; no student identifiers. Sessions count by their server start, completions by the time the server recorded them, and each assignment only from its `assignedAt` on (earlier play isn't class work): per-assignment figures, `days` and `totals` alike. `medianTimeMs`/`medianHints` (each completer's first completion in the range) are null when fewer than 3 students completed. `from`/`to` are inclusive UTC days: default the last 30 days ending today, at most 366 days (400 `ANALYTICS_RANGE_INVALID`), clamped to the organization plan's analytics retention and to today (`range.clamped`); nothing left is 422 `ANALYTICS_RANGE_OUTSIDE_RETENTION`. Works in frozen organizations.
post/v1/classes/{id}/archive
Archive a class
Parameters:
id(path, required)Responses: 200, 401, 403, 404, default
Details
Idempotent. Clears the join code; the class's student tokens stop working until it is restored.
post/v1/classes/{id}/join-code
Rotate the join code
Parameters:
id(path, required)Responses: 201, 400, 401, 403, 404, 409, 422, default
Details
A new code, shown only in this response; the previous one stops working. Never `Idempotency-Key`.
delete/v1/classes/{id}/join-code
Turn the join code off
Parameters:
id(path, required)Responses: 204, 401, 403, 404, default
Details
Idempotent. Nobody can join until a new code is rotated.
get/v1/classes/{id}/progress
The class's progress per student and assignment
Parameters:
id(path, required),filter[publicationId](query),filter[studentId](query)Responses: 200, 400, 401, 403, 404, default
Details
For each student and each of the class's current assignments they started: status, attempts, completion, time, hints and reveals (E20-T03). Sparse: pairs without an entry were not started. Active students and revoked ones whose play is still retained. `filter[publicationId]` (an assignment of the class, else 404 `CLASS_ASSIGNMENT_NOT_FOUND`) and `filter[studentId]` (a student of the class, else 404 `STUDENT_NOT_FOUND`) narrow it.
get/v1/classes/{id}/publications
The class's assigned publications, newest first
Parameters:
id(path, required),limit(query),cursor(query)Responses: 200, 400, 401, 403, 404, default
post/v1/classes/{id}/publications
Assign a publication to the class
Parameters:
id(path, required)Responses: 200, 201, 400, 401, 403, 404, 409, 422, default
Details
One of the organization's own publications; its students may then play it while it is published. Students play only casual publications unless `competitive` is set (a competitive publication assigned without it is not playable for them). 201 when assigned, 200 when it already was (`competitive`, when sent, is changed).
delete/v1/classes/{id}/publications/{publicationId}
Remove a publication from the class
Parameters:
id(path, required),publicationId(path, required)Responses: 204, 401, 403, 404, default
post/v1/classes/{id}/restore
Restore an archived class
Parameters:
id(path, required)Responses: 200, 401, 403, 404, 422, default
Details
Idempotent. Its students play again; rotate the join code to let new ones join.
get/v1/classes/{id}/students
The class's students, in join order
Parameters:
id(path, required),limit(query),cursor(query),filter[status](query)Responses: 200, 400, 401, 403, 404, default
post/v1/classes/join
Join a class with its code and a nickname
Responses: 201, 400, 404, 409, 422, 429, default
Details
No account, email or other personal data. Returns the student token once (`ppst_…`): keep it on the device and send it as `Authorization: Bearer` without cookies. After too many wrong codes from one network, joins from it answer 429 `CLASS_JOIN_PAUSED` for up to 10 minutes.
get/v1/organizations/{id}/classes
The organization's classes, newest first
Parameters:
limit(query),cursor(query),filter[status](query)Responses: 200, 400, 401, 403, 404, default
post/v1/organizations/{id}/classes
Create a class with its first join code
Responses: 201, 400, 401, 403, 404, 422, default
Details
School organizations with a current DPA only. The join code is in this response only (stored hashed); never `Idempotency-Key`, so it is not stored for replays.
get/v1/student/me
The student: nickname, class and school
Responses: 200, 400, 401, 403, default
get/v1/student/progress
Your own progress on your class’s assignments
Responses: 200, 400, 401, 403, default
Details
One entry per assigned publication you started (none: not started), with `openSessionId` to resume (`GET /v1/play-sessions/:id` with the student token). Play itself uses the play-session routes with the student token; progress is always saved on the server.
get/v1/student/publications
The class's assigned publications playable now, newest first
Parameters:
limit(query),cursor(query)Responses: 200, 400, 401, 403, default
Details
Published, not taken down and standalone-playable, of any visibility (private ones play without checks). Load each through its `playPath` with the student token; nothing else is playable with it.
patch/v1/students/{id}
Rename a student (moderate a nickname)
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, 409, 422, default
Details
Same nickname rules as joining; revoked students are 404.
post/v1/students/{id}/revoke
Revoke a student
Parameters:
id(path, required)Responses: 204, 401, 403, 404, default
Details
Idempotent; works in frozen organizations. The student's token stops working at once and the nickname is freed; their play history stays.
search
get/v1/search
Search public puzzles
Parameters:
limit(query),cursor(query),q(query),filter[gameType](query),filter[language](query),filter[difficulty](query),filter[tag](query),sort(query)Responses: 200, 400, default
Details
Public, published puzzles by text (full-text per language, typo-tolerant titles, accents ignored) and filters. Never returns content, checks or solutions.
get/v1/search/profiles
Search creators and organizations
Parameters:
limit(query),cursor(query),q(query, required),filter[kind](query)Responses: 200, 400, 429, default
Details
Users with a public profile and organizations that have at least one public, published puzzle, by handle (prefix) or name (typo-tolerant).
series
get/v1/series
Parameters:
limit(query),cursor(query),ownerOrgId(query),filter[gameType](query)Responses: 200, 400, 401, 403, 404, default
post/v1/series
Responses: 201, 400, 401, 403, 404, 409, default
get/v1/series/{id}
Parameters:
id(path, required)Responses: 200, 401, 403, 404, default
patch/v1/series/{id}
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, 409, default
delete/v1/series/{id}
Parameters:
id(path, required)Responses: 204, 401, 403, 404, default
get/v1/series/{id}/editions
Parameters:
id(path, required),limit(query),cursor(query),from(query),to(query),sort(query)Responses: 200, 400, 401, 403, 404, default
put/v1/series/{id}/editions/{date}
Parameters:
id(path, required),date(path, required),If-Match(header)Responses: 200, 201, 400, 401, 402, 403, 404, 409, 412, 413, 422, 503, default
sessions
get/v1/users/me/devices
Parameters:
limit(query),cursor(query)Responses: 200, 401, 403, default
delete/v1/users/me/devices/{id}
Parameters:
id(path, required)Responses: 204, 401, 403, 404, default
get/v1/users/me/identities
Responses: 200, 401, 403, default
post/v1/users/me/identities/google
Responses: 204, 401, 403, 409, default
delete/v1/users/me/identities/google
Responses: 204, 401, 403, 404, 422, default
post/v1/users/me/reauthenticate
Responses: 204, 400, 401, 403, 429, default
get/v1/users/me/sessions
Parameters:
limit(query),cursor(query)Responses: 200, 401, 403, default
delete/v1/users/me/sessions/{id}
Parameters:
id(path, required)Responses: 204, 401, 403, 404, default
post/v1/users/me/sessions/revoke-others
Responses: 200, 401, 403, default
StripeWebhook
post/v1/webhooks/stripe
Stripe webhook (called by Stripe only)
Parameters:
stripe-signature(header, required)Responses: 200, 400, 503, default
Details
Signed by Stripe (`Stripe-Signature`); idempotent on the event id. Not for API clients.
support
get/v1/support/tickets
Parameters:
limit(query),cursor(query),filter[status](query)Responses: 200, 400, 401, 403, default
post/v1/support/tickets
Open a support ticket
Responses: 201, 400, 401, 403, 422, 429, default
Details
Needs an active paid plan (Creator or Organization Owner; 403 `ENTITLEMENT_REQUIRED` otherwise). The description becomes the first message; the support list is mailed. At most 10 unresolved tickets and 10 new ones per 24 hours (422 `TICKET_LIMIT_REACHED`).
get/v1/support/tickets/{id}
Parameters:
id(path, required)Responses: 200, 401, 403, 404, default
post/v1/support/tickets/{id}/attachments
Register an uploaded attachment
Parameters:
id(path, required)Responses: 201, 400, 401, 403, 404, 409, 422, default
Details
The server checks the bytes against the declared type; images are re-encoded, videos stored as uploaded. Rejected files are discarded (422).
post/v1/support/tickets/{id}/close
Close your resolved ticket
Parameters:
id(path, required)Responses: 200, 401, 403, 404, 409, default
Details
resolved → closed (409 `TICKET_INVALID_TRANSITION` from any other status; closing a closed ticket is a no-op).
post/v1/support/tickets/{id}/messages
Reply on your ticket
Parameters:
id(path, required)Responses: 201, 400, 401, 403, 404, 409, 422, 429, default
Details
Only while the ticket is open, in progress or waiting on you (409 `TICKET_NOT_OPEN`); a ticket waiting on you goes back in progress.
post/v1/support/tickets/{id}/uploads
Get a signed upload URL for an attachment
Parameters:
id(path, required)Responses: 201, 400, 401, 403, 404, 409, 422, 429, default
Details
Private bucket only. Images (JPEG, PNG, WebP, AVIF, GIF…) up to 10 MB, videos (MP4, WebM, QuickTime) up to 50 MB. At most 5 attachments per ticket, pending uploads included (422 `TICKET_ATTACHMENTS_LIMIT`). Then `PUT` the file and register it with `POST /v1/support/tickets/:id/attachments`.
Usage
get/v1/usage
Responses: 200, 401, 403, default
users
get/v1/users/me/appeals
Your appeals, newest first
Parameters:
limit(query),cursor(query),filter[status](query)Responses: 200, 400, 401, 403, default
post/v1/users/me/appeals
Appeal a moderation decision
Responses: 201, 400, 401, 403, 404, 409, 422, 429, default
Details
A ban of yours (any scope; works while platform-banned), a takedown of your content (or of an organization you own), a platform moderator hiding your comment, or a warning. Once per decision and reviewer, within 30 days of the decision, while it still applies. Organization bans go to that organization’s admins first; once they reject it, appeal the same ban again within 30 days to go to platform moderation. Your text is plain text. You get `moderation.appeal_submitted`, then `moderation.appeal_accepted|appeal_rejected`.
get/v1/users/me/appeals/{id}
Parameters:
id(path, required)Responses: 200, 401, 403, 404, default
get/v1/users/me/bans
Your bans
Parameters:
limit(query),cursor(query),filter[status](query)Responses: 200, 400, 401, 403, default
Details
Active ones by default. Works while platform-banned: when any other route answers 403 `USER_BANNED`, show these (scope, organization, rule broken, expiry) with their appeal (`appeal`, `appealable`: appeal with `POST /v1/users/me/appeals`).
get/v1/users/me/moderation-decisions
Moderation decisions about you that can be appealed
Parameters:
limit(query),cursor(query)Responses: 200, 400, 401, 403, default
Details
Warnings, platform hides of your comments and takedowns of your content (or of organizations you own), newest first, each with whether it still applies, its newest appeal and whether a new one can go now. Bans are in `GET /v1/users/me/bans`.
get/v1/users/me/settings
Responses: 200, 401, 403, default
patch/v1/users/me/settings
Change your privacy settings
Responses: 200, 400, 401, 403, default
Details
Partial update. Also while platform-banned, but then only to restrict what others see: turning `profilePublic` or `leaderboardVisible` on is 403 `USER_BANNED` (`details[].field`).
Users
patch/v1/users/avatar
Responses: 200, 400, 401, 403, default
patch/v1/users/info
Responses: 200, 401, 403, 409, default
get/v1/users/me
Responses: 200, 401, 403, default
patch/v1/users/password
Responses: 200, 400, 401, 403, 429, default
webhooks
get/v1/webhook-deliveries/{id}
A delivery with its body and attempts
Parameters:
id(path, required)Responses: 200, 401, 403, 404, default
post/v1/webhook-deliveries/{id}/redeliver
Redeliver
Parameters:
id(path, required)Responses: 202, 400, 401, 403, 404, 409, 503, default
Details
Queues one more attempt with the same event id and body (receivers deduplicate on `Puzzle-Event-Id`).
get/v1/webhook-endpoints
Parameters:
limit(query),cursor(query),ownerOrgId(query)Responses: 200, 400, 401, 403, 404, default
post/v1/webhook-endpoints
Register a webhook endpoint
Responses: 201, 400, 401, 403, 404, 422, 503, default
Details
Returns the signing secret once (`secret`); it is stored encrypted. Endpoints receive `POST` JSON bodies `{ id, type, version: "1", createdAt, workspace, data }` signed with `Puzzle-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>` (headers `Puzzle-Event-Id`, `Puzzle-Event-Type`, `Puzzle-Delivery-Id`), for the events they subscribe to in their own workspace. Retries: first attempt + 8 over ~21.6 h; deduplicate on `id`. Payloads carry ids and public publication settings only: never solutions, play states, comment bodies or who played or commented. | Event | Body schema | Sent when | |---|---|---| | `publication.published` | `PublicationPublishedWebhookDto` | A publication was published (it is playable unless moderation took it down). | | `publication.unpublished` | `PublicationUnpublishedWebhookDto` | A published publication stopped being published: unpublished by the workspace, or closed because its puzzle was deleted (`reason`). | | `publication.archived` | `PublicationArchivedWebhookDto` | A publication was archived (`wasPublished`: it was published until then). | | `completion.created` | `CompletionCreatedWebhookDto` | A player completed a publication (verified against the solution on the server). Anonymous: no player identity. Not sent for school students. | | `comment.created` | `CommentCreatedWebhookDto` | A comment or reply was posted on a publication. Ids only: read the comment through the API. | | `moderation.publication_taken_down` | `PublicationTakenDownWebhookDto` | Platform moderation took the publication down: it is no longer served until restored. | | `moderation.publication_restored` | `PublicationRestoredWebhookDto` | Platform moderation restored a publication it had taken down (`status` tells whether it is published). |
get/v1/webhook-endpoints/{id}
Parameters:
id(path, required)Responses: 200, 401, 403, 404, default
patch/v1/webhook-endpoints/{id}
Update a webhook endpoint
Parameters:
id(path, required)Responses: 200, 400, 401, 403, 404, 422, default
Details
URL, subscriptions, description; `enabled: false|true` disables or re-enables it (re-enabling resets the failure count).
delete/v1/webhook-endpoints/{id}
Parameters:
id(path, required)Responses: 204, 401, 403, 404, default
get/v1/webhook-endpoints/{id}/deliveries
Delivery log of an endpoint (last 30 days)
Parameters:
id(path, required),limit(query),cursor(query),filter[status](query)Responses: 200, 400, 401, 403, 404, default
post/v1/webhook-endpoints/{id}/rotate-secret
Rotate the signing secret
Parameters:
id(path, required)Responses: 201, 401, 403, 404, 503, default
Details
Replaces the secret at once and returns the new one (once); later attempts are signed with it.
post/v1/webhook-endpoints/{id}/test
Send a test event
Parameters:
id(path, required)Responses: 202, 400, 401, 403, 404, 409, 503, default
Details
Queues one `webhook.ping` delivery (a single attempt, no retries); follow it in the delivery log.