Per-endpoint quick reference for every route on /api/v1/. For the long-form integration guide (auth, idempotency, error envelope, worked examples) see the API integration guide. For change history see the API changelog. For a copy-pasteable end-to-end first-call walk-through see API getting started.
Status: pre-release. The endpoints in this reference are stable. If you need one of the routes flagged Phase 3+ in advance of GA, get in touch.
The summary table groups endpoints by domain. Each row links to the per-endpoint anchor below if the route has notes worth surfacing; simple GETs that are obvious from the summary alone have no anchor section.
Summary table
Domain 1: Authentication & Session
| Method | Path | Summary | Authz floor | Idempotent |
|---|---|---|---|---|
| POST | /auth/token/ | Mint a bearer token from a logged-in session. | Session-auth bootstrap | Y |
| DELETE | /auth/token/ | Revoke the calling token. | Token only (no session) | Y |
| GET | /auth/me/ | Current identity + auth method. | Any authed | N |
| POST | /me/sessions/revoke/ | Revoke all tokens for the calling user. | Any authed | Y |
Domain 2: Identity & Authorization
| Method | Path | Summary | Authz floor | Idempotent |
|---|---|---|---|---|
| GET | /me/ | Current user profile. | Any authed | N |
| PATCH | /me/ | Update name and phone number. | Any authed (self) | Y |
| GET | /me/quotas/ | Per-plan slot snapshot + storage usage. | Any authed | N |
| GET | /books/{slug}/permissions/ | Role + feature flags + permitted-action list for a book. | Any authed | N |
Domain 3a: Core Product , Books
| Method | Path | Summary | Authz floor | Idempotent |
|---|---|---|---|---|
| GET | /books/ | List books where caller is owner or admin. | Owner/admin (per row) | N |
| POST | /books/ | Create a book (quota-only , no Stripe). | Authed; quota gates | Y |
| GET | /books/{slug}/ | Read a book. | Owner or admin | N |
| PATCH | /books/{slug}/ | Update book fields (subtitle, theme, settings). | Owner or admin | Y |
| POST | /books/{slug}/archive/ | Archive a book (hide from listings; preserve data). | Owner only | Y |
| POST | /books/{slug}/restore/ | Restore an archived book. | Owner only | Y |
| DELETE | /books/{slug}/ | Permanently delete a book. | Owner only | Y |
Domain 3b: Core Product , Submissions
| Method | Path | Summary | Authz floor | Idempotent |
|---|---|---|---|---|
| POST | /books/{slug}/submissions/ | Submit a memory (text and/or media). | Owner or admin | Y |
| GET | /books/{slug}/submissions/ | List submissions (cursor + delta-sync). | Owner or admin | N |
| GET | /books/{slug}/submissions/{id}/ | Read a submission. Carries etag. | Owner or admin | N |
| PATCH | /books/{slug}/submissions/{id}/ | Edit a submission (optional If-Match). | Owner / admin / submission author | Y |
| DELETE | /books/{slug}/submissions/{id}/ | Permanently delete a submission. | Owner or admin | Y |
| POST | /books/{slug}/submissions/{id}/approve/ | Approve a pending submission. | Owner or admin | Y |
| POST | /books/{slug}/submissions/{id}/reject/ | Reject a pending submission. | Owner or admin | Y |
| POST | /books/{slug}/submissions/{id}/pin/ | Pin an approved submission. | Owner or admin | Y |
| POST | /books/{slug}/submissions/{id}/unpin/ | Unpin a pinned submission. | Owner or admin | Y |
Domain 3c: Core Product , Media
| Method | Path | Summary | Authz floor | Idempotent |
|---|---|---|---|---|
| POST | /books/{slug}/submissions/{id}/media/ | Upload media (multipart; per-kind cap, 90 MB ceiling). | Owner / admin / submission author | Y |
| GET | /books/{slug}/submissions/{id}/media/{media_id}/ | Read media metadata + signed download_url. | Owner or admin | N |
| DELETE | /books/{slug}/submissions/{id}/media/{media_id}/ | Detach + queue cleanup (resets moderation). | Owner or admin | Y |
Domain 3d: Core Product , Exports
| Method | Path | Summary | Authz floor | Idempotent |
|---|---|---|---|---|
| POST | /books/{slug}/exports/ | Request an async export (returns 202). | Owner or admin | Y |
| GET | /books/{slug}/exports/{job_id}/ | Poll export job status. | Owner or admin | N |
| GET | /books/{slug}/exports/{job_id}/artifact/ | Get a 15-min signed download URL. | Owner or admin | N |
Domain 4: Collaboration
| Method | Path | Summary | Authz floor | Idempotent |
|---|---|---|---|---|
| GET | /books/{slug}/members/ | List members (cursor + delta-sync). | Owner or admin | N |
| POST | /books/{slug}/members/ | Add a member by email or user_id. | Owner or admin | Y |
| DELETE | /books/{slug}/members/{user_id}/ | Remove a member (owner-only). | Owner only | Y |
| GET | /books/{slug}/invitations/ | List invitations (cursor + status filter). | Owner or admin | N |
| POST | /books/{slug}/invitations/ | Create an invitation (email / phone / telegram). | Owner or admin | Y |
| DELETE | /books/{slug}/invitations/{id}/ | Soft-revoke a pending invitation. | Owner or admin | Y |
Domain 5: Messaging & Notifications
| Method | Path | Summary | Authz floor | Idempotent |
|---|---|---|---|---|
| GET | /books/{slug}/channels/ | Per-channel status snapshot (no secrets). | Owner or admin | N |
| GET | /books/{slug}/notifications/subscriptions/ | List subscriptions (active + inactive). | Owner or admin | N |
| DELETE | /books/{slug}/notifications/subscriptions/{id}/ | Soft-unsubscribe a subscription. | Owner / admin / subscription owner | Y |
Domain 6: Billing & Plan
| Method | Path | Summary | Authz floor | Idempotent |
|---|---|---|---|---|
| GET | /plans/ | Public plan catalog (unauthenticated). | None | N |
| GET | /books/{slug}/billing/ | Latest billing audit + plan + quota snapshot. | Owner only | N |
Domain 7: Account & GDPR
| Method | Path | Summary | Authz floor | Idempotent |
|---|---|---|---|---|
| POST | /me/export/ | Queue a GDPR data export (async). | Self (authed) | Y |
| GET | /me/export/{job_id}/ | Poll export job; signed URL re-issued on each poll. | Self only | N |
| POST | /me/delete/ | Delete your account (step-up: reauth grant or session password). | Self (authed) | Y |
| POST | /me/delete/reauth/ | Mint a single-use 5-minute delete reauth grant (password or OTP). | Self (authed) | N |
| POST | /me/delete/reauth/otp/ | Request a delete-purpose OTP to your own email/phone. | Self (authed) | N |
| POST | /me/unsubscribe/{stream}/ | Unsubscribe from a per-stream email flow (authenticated). | Self (authed) | Y |
| POST | /me/unsubscribe/{stream}/token/ | RFC 8058 one-click unsubscribe (onboarding only). | RFC 8058 token | N/A |
Total: 46 routes across 7 domains. All endpoints share the /api/v1/ prefix, the canonical error envelope, the Idempotency-Key contract on non-GETs, and the per-route plan-feature gate (api_access on the book's plan).
Endpoint notes
Anchors below cover endpoints whose semantics need more than a one-liner. Routes not listed here are documented sufficiently by their summary row plus the cross-cutting conventions in the integration guide.
POST /auth/token/
Mints a new APIKey bound to the calling user. Bootstrap requirement: the caller must be in a logged-in browser session , the API cannot issue its own first token (a APIKey auth handler needs a token to authenticate, which would be circular). Response carries the plaintext token in the plaintext field exactly ONCE; it is never recoverable afterward. Idempotent replay returns plaintext: null + replayed: true so a retry after a network blip does not crash. The name field must be unique per user; collisions return 409 name_taken.
DELETE /auth/token/
Revokes the token that authenticated the call. Session-auth callers get 401 , there is no "current token" to revoke from a browser. APIKeyAuth.authenticate stashes the calling token on request.api_key so the route can identify it. Idempotent: replay returns 200 without re-running the UPDATE.
POST /me/sessions/revoke/
Stolen-device cascade. Revokes all active tokens for the calling user. Session login is unaffected. Response includes revoked_count; idempotent replay returns the same count without re-running the UPDATE. Audit log emits event="api_sessions_revoked" with request_id for incident response.
PATCH /me/
Updates first_name and last_name only. Email, phone_number, and the channel identifiers (telegram_chat_id, telegram_username, imessage_identifier) are immutable here , each has its own verification flow, so supplying any of them returns 400 validation_failed. Set your phone via the verified phone-link flow on the web UI.
GET /books/{slug}/permissions/
Single Membership lookup + in-memory action enumeration. Returns {role, feature_flags, permitted_actions}. Use this to drive UI affordances client-side instead of try/catching every endpoint for 403. Feature flag feature_ai_enhancer matches the underlying model field name (NOT feature_ai_enhancement). Non-members on non-public books get 404, not 403, to block enumeration.
GET /books/
Cursor-paginated. Response shape: {items, next_cursor, has_more} (no count , counting at pagination scale is expensive and a moving target). Pass the previous response's next_cursor back to fetch the next page; has_more: false means the end. Supports ?updated_since= for delta sync; pin to the response's Last-Modified header, not your client clock.
POST /books/
Quota-only path. If the calling user holds a quota slot at the requested plan tier (admin grant or Package purchase), the book is created directly. If no slot is available AND the plan is non-default, returns 402 payment_required with a hint URL pointing at the web checkout. The API does NOT initiate Stripe checkout , acquire slots via the web UI first.
PATCH /books/{slug}/
Partial update of book fields. Optional If-Match precondition for lost-update protection (refetch + retry on 412 precondition_failed). use_case_slug is translated to FK server-side. During a billing grace period, any write returns 403 book_grace_period.
POST /books/{slug}/archive/
Owner-only. Hides the book from listings; preserves all data. Idempotent , archiving an already-archived book is a no-op + cached 200. Restore via POST /restore/.
DELETE /books/{slug}/
Permanent. Owner-only. Two-step contract when the off-provider backup-erasure runner is unreachable: route returns 202 deletion_queued; retry the same Idempotency-Key after a backoff window OR poll GET /books/{slug}/ until it 404s. The destructive cascade dispatches via Celery on commit, never inline.
POST /books/{slug}/submissions/
Creates a memory. Accepts text, contributor_name, contributor_email, visibility (default public). 24h cache + DB dedup via the canonical _build_dedup_identity helper , replaying the same Idempotency-Key returns the original result (success or failure) without re-executing the side effect. To attach media, follow up with POST /media/.
GET /books/{slug}/submissions/
Cursor-paginated. Supports ?updated_since=<iso8601> for delta sync; composes with ?cursor=. Each item carries an etag for round-tripping into If-Match on the next PATCH. Visibility filter applied server-side via SubmissionQuerySet.visible_to , clients never see rows they cannot read.
PATCH /books/{slug}/submissions/{id}/
The one route in this domain where contributors can act , a contributor-role member who authored the submission can edit their own row. Moderation actions (approve/reject/pin/unpin/delete) stay owner/admin only. Optional If-Match: when present, 412 on mismatch; when omitted, last-write-wins. Adding or removing media via this route resets moderation to pending and unpins the submission BEFORE the media mutation lands.
POST /books/{slug}/submissions/{id}/approve/
Wraps the moderation service through _moderation_action_json: BookGracePeriodError → 423, ValidationError → 400, LimitExceeded → 402. Idempotent , approving an already-approved submission is a 200 + cached response.
POST /books/{slug}/submissions/{id}/reject/
Body shape: {"reason": "<text>"}. Empty body is accepted (parsed manually since Ninja's typed-schema params reject empty bodies). The reason field surfaces to the contributor as part of the rejection notification.
POST /books/{slug}/submissions/{id}/media/
Multipart upload. Accepts image/*, audio/*, video/*. Photos up to 20 MB, audio up to 50 MB, video up to your plan's limit; the multipart request ceiling is 90 MB. Either case returns 413 before any bytes are persisted. MIME is detected server-side via magic-byte sniff (_detect_mime_type); the client-declared Content-Type is advisory only.
Owner/admin OR the submission's contributor_user can call this. edit_submission(media_will_change=True) runs BEFORE create_media in the same atomic block , moderation resets to pending, share_card_file clears, and the submission unpins before the new bytes are visible. Plan feature gates apply: audio uploads need feature_audio; video uploads need feature_video or feature_live_photo (a feature_live_photo-only plan accepts short QuickTime motion only, 6 seconds or less, not arbitrary video); plans without the feature return 400 validation_failed.
GET /books/{slug}/submissions/{id}/media/{media_id}/
Response carries download_url, download_status, processing_status, mime_type, scan_result. The URL comes from resolve_delivery() + issue_media_signed_url() , never inline processed_file or original_file. On S3-compatible storage (R2 in production) the URL is a 15-minute presigned URL , carries an X-Amz-Expires=900 query parameter. Clients MUST NOT cache download_url; refetch via this endpoint when the URL expires. If the storage backend refuses to sign for the file (fail-closed), the response carries download_url: null and download_status: "unavailable" , poll until it becomes available. Audio and video files write scan_result: "skipped_by_policy" (not "clean"); the serializer serves them regardless, so clients should NEVER gate playback on scan_result == "clean" alone. Poll until processing_status == "complete" before surfacing the URL. Video rows also carry thumbnail_url: a signed URL for the poster frame 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 MediaRead and the MediaSummary embedded in submission reads.
DELETE /books/{slug}/submissions/{id}/media/{media_id}/
Owner-or-admin. edit_submission(media_will_change=True) runs BEFORE the delete in the same atomic block so moderation resets correctly. CASCADE FK + the existing pre_delete signal handle storage cleanup (file blobs, HLS dirs, external video assets, storage counters). Idempotent , replay returns cached 204.
POST /books/{slug}/exports/
Returns 202 + an ExportRead row; the Celery generate_export task renders out of band. Fields:
theme: required. One ofelegant_classic,modern_clean,storybook,memorial_service,wedding_album,baby_first_year,daily_journal,retirement_tribute,botanical_luxe,heirloom_gallery,editorial_noir. Unknown themes return 400 with the valid list indetails.offered.intent:screen(default; 0 bleed, 0 gutter),home(0 bleed, 10 gutter),shop(3 bleed, 15 gutter).export_format:pdf(default),json,zip. Each format requires a matching plan feature.
Always uses the v2 render path regardless of the export_themes_v2 Waffle switch.
GET /books/{slug}/exports/{job_id}/artifact/
Returns a 15-minute signed download URL. Uses boto3 generate_presigned_url on S3-compatible backends (bypasses R2's custom domain so the TTL is enforced); falls back to file_field.url for local storage only (fails closed on S3 , see 502 below). Status codes:
200:{download_url, expires_at}.404: job_id does not exist (cross-book id traversal also surfaces as 404).409 export_not_ready: status is stillpendingorprocessing; poll the status endpoint.410 export_failed: job failed or the output file is no longer available; request a new export.502 storage_unavailable: the artifact still exists but signed-URL minting failed transiently (storage outage, credential rotation, region mismatch). Retry with exponential backoff (1s, 2s, 4s, 8s, …). Distinct from410, where the file itself is gone.
The same 502 storage_unavailable envelope is returned by GET /me/export/{id}/ for completed GDPR data-export jobs whose signed URL cannot be minted; same retry contract.
POST /books/{slug}/members/
Accepts email OR user_id (exactly one). role defaults to admin; must be admin or contributor (owner-role transfer is not exposed). Error codes: 404 user_not_found, 409 already_a_member, 402 member_limit_exceeded when the plan cap is hit.
DELETE /books/{slug}/members/{user_id}/
Owner-only. Returns 204 on success and on already-gone (idempotent). Removing the book owner returns 400 cannot_remove_owner (route-layer pre-check + service-layer last-owner backstop).
GET /books/{slug}/invitations/
Optional ?status= filter narrows to pending / accepted / expired / revoked. Unknown status → 400 query_param_invalid. Supports ?updated_since= for delta-sync.
POST /books/{slug}/invitations/
Fields: identifier_type ∈ {email, phone, telegram}, identifier, role (defaults to contributor). Service-layer normalisation: lowercase emails, E.164 phones via normalize_phone, strip leading @ + lowercase Telegram handles.
When the identifier matches an existing user, a Membership is created eagerly and the returned row carries status="accepted". The eager branch consults check_member_limit → 402 member_limit_exceeded on plan-cap miss. Phone/Telegram invitations activate via channel inbound only , there is no API call to "send the invite," the create endpoint owns both creation and dispatch.
DELETE /books/{slug}/invitations/{id}/
Soft-revoke: flips status to "revoked" instead of hard-deleting (audit trail survives). Idempotent: re-revoke returns 200 with already_revoked: true. The partial unique constraint on (book, identifier_type, identifier) only covers status="pending", so a revoked row does not block re-inviting the same identifier later.
GET /books/{slug}/channels/
Per-channel projection of get_channel_status(book) through a strict ChannelStatusRead schema. Sensitive provider config (API keys, webhook secrets) is NEVER exposed , only contributor-facing values (public phone numbers, mailto: addresses, deep links). Returns one row per channel: web, email, sms, rcs, imessage, whatsapp, telegram.
DELETE /books/{slug}/notifications/subscriptions/{id}/
Soft-delete: flips is_active to False. Idempotent: re-delete returns 200 with already_inactive: true. The subscription's own user can always cancel their own subscription regardless of book Membership.
POST /me/export/
Queues a GDPR data export. Returns 202 + a pending job row; poll GET /me/export/{job_id}/ until status="complete", then read download_url (60-minute signed URL re-issued on every poll). Rate-limited 1 per day per user: a second request within 24h returns 429 rate_limited even with a different Idempotency-Key. Export retention is 30 days in R2.
POST /me/delete/
Two-phase. Phase 1 (synchronous PII scrub on the user row) runs inside the request; Phase 2 (heavy cascade , owned books, history rows, off-provider backup erasure publish) dispatches as a Celery task on commit.
Requires step-up reauthentication: a bearer credential alone is rejected with 403 reauth_required. Either send reauth_token (a single-use, 5-minute grant minted by POST /me/delete/reauth/ after presenting your account password, or a delete-purpose OTP 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 re-spend the grant. If the off-provider erasure runner is unreachable, returns 202 deletion_queued; retry the same Idempotency-Key after a backoff window (the grant is restored for the retry).
POST /me/unsubscribe/{stream}/
Authenticated route (session or Bearer token, same as every other /me/ route). Supported streams:
onboarding: post-signup reminder stream.digests: per-book digests on books you own.activity_reminders: inactivity nudges on books you own.
Unknown streams return 400 unknown_stream with the supported list in details.offered. Idempotent: 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 our onboarding emails resolve here with ?token=<uuid> against OnboardingUnsubscribe.token. Supports the onboarding stream ONLY because that's the only stream that issues RFC 8058 tokens. Owner-scoped streams (digests, activity_reminders) return 400 stream_not_token_supported on this path , use the authenticated route for those instead.
Missing or invalid token returns 401 unauthorized. The token itself is the dedup identity, so no Idempotency-Key header is required.
Cross-cutting conventions
Every endpoint on /api/v1/ shares these contracts:
- Error envelope. All 4xx and 5xx use
{code, message, details, request_id}. Branch oncode, notmessage. See the integration guide. - Idempotency. Every non-GET requires the
Idempotency-Keyheader. Missing or empty returns 428idempotency_key_required. 24h cache scope. - Feature gate. Every protected route requires the calling user's book to have the
api_accessplan feature. Plans without it return 403feature_gated. - Authorization floor. All non-public routes resolve through
apps/accounts/policy.py:BookPermission. Contributor-role memberships do not have general API access; the one carve-out isPATCH /submissions/{id}/where the submission'scontributor_usercan edit their own row. - Cursor pagination. All list endpoints return
{items, next_cursor, has_more}. Passnext_cursorback as?cursor=. - Delta sync. Most list endpoints accept
?updated_since=<iso8601>. Pin to the response'sLast-Modifiedheader, not your client clock. - Optimistic locking. Detail GETs carry an
etagfield. Send it back asIf-Matchon the next PATCH for RFC 7232 lost-update protection. - Deprecation headers. Endpoints scheduled for removal carry
DeprecationandSunsetresponse headers per RFC 8594, with a minimum 90-day notice. See the changelog.
Ready to try it? Create your free memory book