Errors
RFC 9457 problem+json responses, the frozen error-code registry, and the 401/403 discipline.
Every error from /public/v1 is an
RFC 9457 problem document:
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json{
"type": "https://developers.mouve.it/errors/insufficient-scope",
"title": "Insufficient scope",
"status": 403,
"code": "INSUFFICIENT_SCOPE",
"detail": "This request requires the posts.write scope.",
"request_id": "req_8f3a1c2b9d4e"
}- Match on
code— stable, UPPER_SNAKE, from the registry below.detailis human wording and may change;codenever does. request_ididentifies the exact request in Mouve's logs — include it in any support report. It is also on every success response as theX-Request-Idheader.VALIDATION_FAILEDadds anerrorsarray of{"field": "...", "message": "..."}items.429problems addscope,window,limit, andretry_after_secondsmembers plus aRetry-Afterheader — see rate limits.
Status discipline
The status code alone tells you the category of your next move:
| Status | Meaning | Your move |
|---|---|---|
401 | The token itself is invalid, expired, or revoked | Refresh (OAuth) or obtain a new token; if that fails, re-authorize |
403 | Token is fine; this action is not permitted | Do not retry, do not refresh — fix scopes or respect the block |
409 | Same idempotency key still in flight | Retry after Retry-After |
422 | Request understood, semantically rejected | Fix the request; retrying unchanged will fail again |
429 | Over a rate limit | Back off per Retry-After |
5xx | Mouve-side fault | Retry with backoff and the same Idempotency-Key |
Registry
Append-only: codes are never removed, renamed, or moved to another status. Additions are announced in the changelog.
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | A field is missing, malformed, or out of range (includes missing audience) |
| 400 | INVALID_CURSOR | Cursor is malformed, expired in format, or minted by a different query |
| 400 | INVALID_IDEMPOTENCY_KEY | Idempotency-Key header fails the format rules |
| 401 | UNAUTHENTICATED | No usable credential on the request |
| 401 | TOKEN_EXPIRED | Access token past its lifetime |
| 401 | TOKEN_REVOKED | Grant disconnected or token revoked — do not retry |
| 403 | INSUFFICIENT_SCOPE | Token lacks a required scope |
| 403 | VENUE_NOT_VERIFIED | The venue has not proven it controls its domain yet (verification tier below t1) |
| 403 | APPROVAL_REQUIRED | The change needs the venue owner to approve it; the response carries approval_url, do not retry |
| 403 | TOKEN_NOT_PERMITTED | Endpoint is not available to API tokens at all |
| 403 | FORBIDDEN | Permitted scope, but this specific resource/action is not allowed |
| 403 | ACCOUNT_SUSPENDED | The acting user's account is suspended |
| 403 | ONBOARDING_REQUIRED | The acting user has not completed onboarding |
| 403 | CLIENT_DISABLED | The app the token belongs to is disabled or taken down |
| 404 | NOT_FOUND | Resource absent — or not visible to the acting user |
| 409 | IDEMPOTENCY_IN_FLIGHT | Same key currently processing; retry shortly |
| 409 | SLOT_UNAVAILABLE | The slot was taken (or is closed) by the time the booking was made; pick another slot |
| 409 | CHANGESET_STALE | The changeset was edited after the approval hash was computed; review it again |
| 409 | CHANGESET_OPEN | Another changeset of the venue is already open (the owner's, or another sign-in's); wait until it is approved, rejected or expired |
| 409 | CONFLICT | The request conflicts with the current state of the resource; read it again before retrying |
| 410 | ENDPOINT_RETIRED | Endpoint passed its published sunset date |
| 413 | PAYLOAD_TOO_LARGE | Body or upload exceeds the size limit |
| 422 | IDEMPOTENCY_BODY_MISMATCH | Same key, different request body |
| 422 | OUTSIDE_HOURS | The booking falls outside the venue opening hours |
| 422 | AUDIENCE_UNAVAILABLE | e.g. audience: "everyone" on a private-profile account |
| 422 | MEDIA_NOT_FOUND | A media_ids entry does not exist or is not yours |
| 422 | MEDIA_ALREADY_ATTACHED | Media id already used by another post |
| 422 | MEDIA_URL_INVALID | Source URL is not fetchable HTTPS |
| 422 | MEDIA_URL_BLOCKED | Source URL is not allowed |
| 422 | MEDIA_FETCH_FAILED | Source URL did not yield a usable image |
| 422 | MEDIA_TOO_LARGE | Image exceeds 10 MB |
| 422 | MEDIA_UNSUPPORTED_TYPE | Not JPEG, PNG, WebP, or GIF |
| 422 | MEDIA_VIDEO_UNSUPPORTED | Video is not supported in v1 |
| 429 | RATE_LIMITED | Per-minute limit exceeded |
| 429 | USAGE_LIMIT_EXCEEDED | Daily write cap exceeded |
| 429 | MEDIA_QUOTA_EXCEEDED | Rolling media quota exceeded |
| 500 | INTERNAL | Unexpected Mouve-side error |
| 503 | UNAVAILABLE | Temporarily unable to serve; retry with backoff |
| 503 | VENUE_AGENT_WRITES_DISABLED | Writes made with an OAuth or API credential are switched off (venue freeze or the org-wide kill switch); session writes still work |
One deliberate ambiguity: 404 NOT_FOUND is returned both for resources
that do not exist and for resources the acting user may not see — the API
never confirms the existence of content you cannot access.