This page is for software developers building against the Our Memory Book API. If you are here to run your own book, the help index has the guides for owners and contributors.

Per-endpoint quick reference for the integrator (API-key) routes on /api/v1/. The contributor /me/... routes, cover upload, original-file download, transcription-on-request, device and push routes serve the Our Memory Book app and are not part of the integrator contract, so they are not listed here. For the long-form integration guide (auth, idempotency, error envelope, worked examples) see the API integration guide; for a copy-pasteable first call see API getting started; for change history see the API changelog.

Status: pre-release. Every route on this page is stable and safe to build on; removals follow the deprecation policy in the conventions below. Need a route that is not here? Get in touch.

Conventions

Every endpoint on /api/v1/ shares these contracts, and the summary tables assume them:

  • Who can call it. The minimum role per route. Owner or admin means a membership with that role on the book; contributor-role members have no general API access. Any signed-in user means any authenticated caller acting on their own account. All non-public routes resolve through one permission policy; a non-member of a non-public book gets 404, never 403, so a book's existence cannot be probed.
  • Idempotent. Every non-GET requires the Idempotency-Key header (missing or empty: 428 idempotency_key_required). Yes means a replay with the same key within 24 hours returns the original result without repeating the side effect. No means each call has a fresh effect (a new grant, a new code). Read means a GET: no header, nothing to replay.
  • Error envelope. All 4xx and 5xx use {code, message, details, request_id}. Branch on code, not message. See the integration guide. One exception: a body that fails schema validation returns a Django Ninja 422 with a {"detail": [...]} body, not the envelope.
  • Feature gate. Only POST /books/{slug}/submissions/ requires the book's plan to include api_access; plans without it return 403 feature_gated there. Every other route works on every plan.
  • Cursor pagination. All list endpoints return {items, next_cursor, has_more}. Pass next_cursor back as ?cursor=.
  • Delta sync. Most list endpoints accept ?updated_since=<iso8601>. Pin it to the response's Last-Modified header, not your client clock.
  • Optimistic locking. Detail GETs carry an etag field. Send it back as If-Match on the next PATCH for RFC 7232 lost-update protection.
  • Deprecation headers. Endpoints scheduled for removal carry Deprecation and Sunset response headers per RFC 8594, with a minimum 90-day notice. See the changelog.

Summary

Routes are grouped by what they act on. A linked endpoint has a note below with the details a one-line summary cannot carry; unlinked routes are fully described by their row plus the conventions above.

Authentication & session

EndpointWhat it doesWho can call itIdempotent
POST /auth/token/Mint a bearer token from a logged-in session.Browser session onlyYes
DELETE /auth/token/Revoke the calling token.Token only (no session)Yes
GET /auth/me/Current identity and auth method.Any signed-in userRead
POST /me/sessions/revoke/Revoke every token for the calling user.Any signed-in userYes

Identity & authorization

EndpointWhat it doesWho can call itIdempotent
GET /me/Current user profile.Any signed-in userRead
PATCH /me/Update your name.Any signed-in userYes
GET /me/quotas/Per-plan slot snapshot and storage usage.Any signed-in userRead
GET /books/{slug}/permissions/Your role, feature flags and permitted actions on a book.Any signed-in userRead

Books

EndpointWhat it doesWho can call itIdempotent
GET /books/List books where you are owner or admin.Owner or admin (per row)Read
POST /books/Create a book from a held quota slot (no card payment).Any signed-in user; quota gatesYes
GET /books/{slug}/Read a book.Owner or adminRead
PATCH /books/{slug}/Update book fields (subtitle, theme, settings).Owner or adminYes
POST /books/{slug}/archive/Archive a book (hidden from listings, data kept).Owner onlyYes
POST /books/{slug}/restore/Restore an archived book.Owner onlyYes
DELETE /books/{slug}/Permanently delete a book and everything in it.Owner onlyYes

Submissions

EndpointWhat it doesWho can call itIdempotent
POST /books/{slug}/submissions/Submit a memory (text, media, or both).Owner or adminYes
GET /books/{slug}/submissions/List submissions (cursor and delta sync).Owner or adminRead
GET /books/{slug}/submissions/{id}/Read a submission. Carries etag.Owner or adminRead
PATCH /books/{slug}/submissions/{id}/Edit a submission (optional If-Match).Owner, admin, or the authorYes
DELETE /books/{slug}/submissions/{id}/Permanently delete a submission.Owner or adminYes
POST /books/{slug}/submissions/{id}/approve/Approve a pending submission.Owner or adminYes
POST /books/{slug}/submissions/{id}/reject/Reject a pending submission.Owner or adminYes
POST /books/{slug}/submissions/{id}/pin/Pin an approved submission.Owner or adminYes
POST /books/{slug}/submissions/{id}/unpin/Unpin a pinned submission.Owner or adminYes

Media

EndpointWhat it doesWho can call itIdempotent
POST /books/{slug}/submissions/{id}/media/Upload a file (multipart; per-kind cap, 90 MB ceiling).Owner, admin, or the authorYes
GET /books/{slug}/submissions/{id}/media/{media_id}/Read media metadata and a signed download_url.Owner or adminRead
DELETE /books/{slug}/submissions/{id}/media/{media_id}/Detach a file and queue cleanup (resets moderation).Owner or adminYes

Exports

EndpointWhat it doesWho can call itIdempotent
POST /books/{slug}/exports/Request an export (returns 202).Owner or adminYes
GET /books/{slug}/exports/{job_id}/Poll an export job.Owner or adminRead
GET /books/{slug}/exports/{job_id}/artifact/Get a 15-minute signed download URL.Owner or adminRead

Collaboration

EndpointWhat it doesWho can call itIdempotent
GET /books/{slug}/members/List members (cursor and delta sync).Owner or adminRead
POST /books/{slug}/members/Add a member by email or user_id.Owner or adminYes
DELETE /books/{slug}/members/{user_id}/Remove a member.Owner onlyYes
GET /books/{slug}/invitations/List invitations (cursor and status filter).Owner or adminRead
POST /books/{slug}/invitations/Invite by email, phone or Telegram.Owner or adminYes
DELETE /books/{slug}/invitations/{id}/Revoke a pending invitation.Owner or adminYes

Messaging & notifications

EndpointWhat it doesWho can call itIdempotent
GET /books/{slug}/channels/Per-channel status snapshot (no secrets).Owner or adminRead
GET /books/{slug}/notifications/subscriptions/List subscriptions (active and inactive).Owner or adminRead
DELETE /books/{slug}/notifications/subscriptions/{id}/Unsubscribe a subscription.Owner, admin, or the subscriberYes

Billing & plan

EndpointWhat it doesWho can call itIdempotent
GET /plans/Public plan catalog.AnyoneRead
GET /books/{slug}/billing/Latest billing record, plan and quota snapshot.Owner onlyRead

Account & GDPR

EndpointWhat it doesWho can call itIdempotent
POST /me/export/Queue a GDPR data export.Any signed-in userYes
GET /me/export/{job_id}/Poll the export; a fresh signed URL on each poll.The requesting userRead
POST /me/delete/Delete your account (step-up reauthentication required).Any signed-in userYes
POST /me/delete/reauth/Mint a single-use, 5-minute delete grant (password or code).Any signed-in userNo
POST /me/delete/reauth/otp/Send a delete-purpose code to your own email or phone.Any signed-in userNo
POST /me/unsubscribe/{stream}/Unsubscribe from an email stream.Any signed-in userYes
POST /me/unsubscribe/{stream}/token/RFC 8058 one-click unsubscribe (onboarding only).Email tokenYes (token)

The integrator routes, in 10 groups. All share the /api/v1/ prefix, the error envelope and the Idempotency-Key contract on non-GETs; only POST /books/{slug}/submissions/ carries the api_access plan-feature gate.

Endpoint notes

The routes below need more than a one-liner. Everything else is documented by its summary row plus the conventions.

POST /auth/token/

Mints a new API token for the calling user. The first token must be minted from a logged-in browser session: the API cannot issue its own first token. The response carries the plaintext token once, in plaintext; it is never shown again. An idempotent replay returns plaintext: null and replayed: true, so a retry after a network blip does not fail. name must be unique per user; a collision returns 409 name_taken.

DELETE /auth/token/

Revokes the token that authenticated the call. Session-authenticated callers get 401: a browser session has no current token to revoke. Idempotent: a replay returns 200.

POST /me/sessions/revoke/

The stolen-device switch. Revokes every active API token for the calling user; web sessions are unaffected. The response includes revoked_count; a replay returns the same count without revoking anything further.

PATCH /me/

Updates first_name, last_name and books_sort (the shelf order: activity, title, created or role) only. Email, phone_number and the channel identifiers (telegram_chat_id, telegram_username, imessage_identifier) each have their own verification flow, so sending any of them returns 400 validation_failed. Set your phone through the verified phone-link flow on the web.

GET /books/{slug}/permissions/

Returns {book_slug, role, feature_*, actions} for the calling user on one book: role is owner, admin, contributor or null, the feature_* booleans mirror the book's plan, and actions lists the action names the caller may perform. Use it to drive client affordances instead of probing every endpoint for 403. The AI enhancer flag is spelled feature_ai_enhancer. Non-members of a non-public book get 404, not 403.

GET /books/

Cursor-paginated: {items, next_cursor, has_more}. There is no count. Pass next_cursor back as ?cursor=; has_more: false marks the end. ?updated_since= gives delta sync; pin it to the response's Last-Modified header, not your clock.

POST /books/

Creates a book from a quota slot. If you hold a slot at the requested plan tier, the book is created immediately. If no slot is available and the plan is not the default plan, the response is 402 payment_required with a hint URL to the web checkout. The API never starts a card payment: acquire slots on the web first.

PATCH /books/{slug}/

Partial update. Send If-Match with the etag from your last read for lost-update protection; a mismatch returns 412 precondition_failed (refetch and retry). Set the book's purpose with use_case_slug. While the book's subscription is in its grace period every write returns 403 book_grace_period.

POST /books/{slug}/archive/

Owner only. Hides the book from listings and keeps every story, photo and recording. Archiving an already-archived book is a no-op 200. Undo with POST /books/{slug}/restore/.

DELETE /books/{slug}/

Permanent, owner only. This erases every story, photo, recording and export that contributors added to the book, and there is no undo. If the book might be wanted again, archive it instead.

When the deletion has to be queued (the off-site backup erasure step is temporarily unreachable) the response is 202 deletion_queued: retry the same Idempotency-Key after a backoff, or poll GET /books/{slug}/ until it returns 404. The cascade runs after the response, never inside it.

POST /books/{slug}/submissions/

Creates a memory. Fields: text_content (required), contributor_name, is_private (default false); unknown fields are refused with a 422. The contributor email is always the API key owner's. Replaying the same Idempotency-Key returns the original result, success or failure, without creating a second memory. Attach files afterwards with POST /books/{slug}/submissions/{id}/media/.

GET /books/{slug}/submissions/

Cursor-paginated. ?updated_since=<iso8601> gives delta sync and composes with ?cursor=. Each item carries an etag to send back as If-Match on its next PATCH. The list contains only rows the caller may read; there is nothing to filter client-side.

PATCH /books/{slug}/submissions/{id}/

The one route in this group a contributor can call: a contributor-role member may edit a submission they authored. Moderation actions (approve, reject, pin, unpin, delete) stay owner or admin only. If-Match is optional: when present, a mismatch returns 412; when absent, the last write wins. Adding or removing media through this route resets moderation to pending and unpins the submission before the change lands.

POST /books/{slug}/submissions/{id}/approve/

Approves a pending submission. 423 while the book is in its billing grace period, 400 on a validation error, 402 when a plan limit is exceeded. Approving an already-approved submission returns 200 with the cached result.

POST /books/{slug}/submissions/{id}/reject/

Body: {"reason": "<text>"}. An empty body is accepted. reason is shown to the contributor in their rejection notification.

POST /books/{slug}/submissions/{id}/media/

Multipart upload of one file. Accepts image/*, audio/*, video/*. Photos up to 20 MB, audio up to 50 MB, video up to your plan's limit; the request ceiling is 90 MB. An oversize file returns 413 before any bytes are stored. The file type is detected from its bytes; the declared Content-Type is advisory only.

Callable by the owner, an admin, or the submission's author. The upload resets moderation to pending, clears the share card and unpins the submission before the new file becomes visible. Plan gates: audio needs feature_audio; video needs feature_video or feature_live_photo (a feature_live_photo-only plan accepts QuickTime motion of six seconds or less, not arbitrary video). Without the feature: 400 validation_failed.

GET /books/{slug}/submissions/{id}/media/{media_id}/

Response fields: download_url, download_status, processing_status, mime_type, scan_result; video rows also carry thumbnail_url.

  • download_url is a 15-minute presigned URL (it carries X-Amz-Expires=900). Do not cache it; refetch from this endpoint when it expires.
  • Wait for processing_status == "complete" before surfacing the URL.
  • download_url: null with download_status: "unavailable" means signing failed closed; poll until it becomes available.
  • Audio and video carry scan_result: "skipped_by_policy", not "clean", and are served regardless. Never gate playback on scan_result == "clean" alone.
  • thumbnail_url is a signed poster-frame URL with the same 15-minute TTL and the same fail-closed null; it is null for non-video media and until processing completes. Present on both the media read and the MediaSummary embedded in submission reads.

DELETE /books/{slug}/submissions/{id}/media/{media_id}/

Owner or admin. Resets moderation to pending, detaches the file, and queues cleanup of every derived asset and the storage counters. A replay returns the cached 204.

POST /books/{slug}/exports/

Returns 202 and the export job; rendering happens in the background. Fields:

  • theme: required. One of elegant_classic, modern_clean, storybook, memorial_service, wedding_album, baby_first_year, daily_journal, retirement_tribute, botanical_luxe, heirloom_gallery, editorial_noir. An unknown theme returns 400 with the valid list in details.offered.
  • intent: screen (default; no bleed, no gutter), home (no bleed, 10 mm gutter), shop (3 mm bleed, 15 mm gutter).
  • export_format: pdf (default), json, zip. Each format requires a matching plan feature.

GET /books/{slug}/exports/{job_id}/artifact/

Returns a 15-minute signed download URL. Status codes:

  • 200: {download_url, expires_at}.
  • 404: no such job (a job id from another book also surfaces as 404).
  • 409 export_not_ready: status is still pending or processing; poll the status endpoint.
  • 410 export_failed: the job failed or the output file is no longer available; request a new export.
  • 502 storage_unavailable: the file exists but the signed URL could not be minted right now (storage outage, credential rotation). Retry with exponential backoff (1 s, 2 s, 4 s, 8 s). Distinct from 410, where the file itself is gone.

GET /me/export/{job_id}/ returns the same 502 storage_unavailable envelope for a completed GDPR export whose URL cannot be minted; same retry contract.

POST /books/{slug}/members/

Accepts email or user_id (exactly one). role defaults to admin and must be admin or contributor; ownership cannot be transferred here. Error codes: 404 user_not_found, 409 already_a_member, 402 member_limit_exceeded when the plan's member cap is reached.

DELETE /books/{slug}/members/{user_id}/

Owner only. Returns 204 on success and on an already-removed member. Removing the owner returns 400 cannot_remove_owner.

GET /books/{slug}/invitations/

Optional ?status= narrows to pending, accepted, expired or revoked; any other value returns 400 query_param_invalid. Supports ?updated_since= for delta sync.

POST /books/{slug}/invitations/

Fields: identifier_type (email, phone or telegram), identifier, role (default contributor). Identifiers are normalised: emails lower-cased, phones converted to E.164, Telegram handles stripped of a leading @ and lower-cased.

If the identifier already belongs to a user, the membership is created immediately and the returned row has status="accepted"; that path returns 402 member_limit_exceeded when the plan's member cap is reached. Phone and Telegram invitations are accepted when the person messages the book; there is no separate "send" call, this endpoint creates and dispatches the invitation.

DELETE /books/{slug}/invitations/{id}/

Sets status to "revoked" rather than deleting the row, so the audit trail survives. Revoking again returns 200 with already_revoked: true. A revoked invitation does not block re-inviting the same identifier later.

GET /books/{slug}/channels/

One row per channel (web, email, sms, rcs, imessage, whatsapp, telegram) with its contributor-facing details: public phone numbers, mailto addresses, deep links. Provider credentials and webhook secrets are never included.

DELETE /books/{slug}/notifications/subscriptions/{id}/

Deactivates the subscription rather than deleting it. Deleting again returns 200 with already_inactive: true. The subscriber can always cancel their own subscription, whether or not they are a member of the book.

POST /me/export/

Queues a GDPR data export. Returns 202 and a pending job; poll GET /me/export/{job_id}/ until status="complete", then read download_url (a 60-minute signed URL, re-issued on every poll). Limited to one export per day per user: a second request within 24 hours returns 429 rate_limited even with a different Idempotency-Key. Exports are kept for 30 days.

POST /me/delete/

Two phases: your account's personal data is scrubbed inside the request; the books you own, history and the off-site backup erasure follow in the background.

Requires step-up reauthentication: a bearer token alone is rejected with 403 reauth_required. Either send reauth_token (a single-use, 5-minute grant from POST /me/delete/reauth/, minted against your password or a delete-purpose code requested via POST /me/delete/reauth/otp/), or, on a session-authenticated call only, send password. The idempotency replay check runs before the step-up gate, so a retry after a connection blip does not spend the grant twice. If the off-site erasure step is unreachable, the response is 202 deletion_queued; retry the same Idempotency-Key after a backoff (the grant is restored for the retry).

POST /me/unsubscribe/{stream}/

Authenticated (session or bearer token, like every other /me/ route). Streams:

  • onboarding: the post-signup reminder emails.
  • digests: per-book digests on books you own.
  • activity_reminders: inactivity nudges on books you own.

An unknown stream returns 400 unknown_stream with the supported list in details.offered. A second call returns 200 with already_unsubscribed: true. The Idempotency-Key header is required.

POST /me/unsubscribe/{stream}/token/

RFC 8058 one-click path, unauthenticated. The unsubscribe links in onboarding emails resolve here with ?token=<uuid>. Only the onboarding stream issues these tokens; digests and activity_reminders return 400 stream_not_token_supported on this path, so use the authenticated route for those.

A missing or invalid token returns 401 unauthorized. The token itself is the replay identity, so no Idempotency-Key header is needed.

Next

API getting started walks through your first call end to end. The API changelog lists every change with the date it shipped.

Ready to try it? Create your free memory book