openapi: 3.1.0 info: title: Cherished Book — API # Bump on every API change (CONTEXT.md › Maintaining the API, rule 6). # 1.1.0 — memorial list: created_after/created_before filters + pagination. # 1.2.0 — Personal tier: device-flow auth, memorial create/edit/delete, # contribution moderation, cover-photo upload. Additive; the business # endpoints are unchanged. # 1.3.0 — GET /templates: the memorial designs, so template_id can be chosen # rather than guessed. Additive, every tier, no gating. # 1.4.0 — Personal tier: share links that actually grant access, and the # Memorial Book / archive exports. Additive. # 1.5.0 — Personal tier: pin/unpin a tribute, visit counts, the memorial # fund, announcements and events, and their RSVPs. Additive. The # fund and broadcasts are Premium and refuse with 422 when the # memorial is not. version: "1.5.0" description: > The Cherished Book public API. Authenticate with a bearer token. There are two kinds of token and they see different things. A **personal token** acts for one person, on memorials they look after. An AI assistant obtains one through the device flow below — no copying and pasting — and it can then create memorials, edit their wording and settings, upload a photo, and read and moderate the tributes people leave. It can never email anyone, see anyone else's memorials, or publish something the safety check refused. A **business token** is bound to one Organization and scopes every response to that organization's data. A Business Admin self-mints it in Business settings. Use it to provision memorials for the families you look after and manage their invitations. Everything published on Cherished Book is safety-checked by an automated review first. Creating or rewording a memorial therefore returns `202` and a status of `pending` — the memorial is not live until that check passes. Do not tell anyone their memorial is published until a response says so. servers: - url: /api/v1 security: - bearerAuth: [] components: securitySchemes: bearerAuth: type: http scheme: bearer description: > API token, sent as `Authorization: Bearer `. A personal token is obtained through the device flow (`POST /auth/device`), or by hand from Connected apps in account settings. Rate limited to 20 requests/minute. A business token is created by a Business Admin in Business settings → API tokens, is shown once, and scopes to that organization. Rate limited to 50 requests/minute. schemas: ExportState: type: object required: [export_id, type, status, ready, message] properties: export_id: { type: integer } type: { type: string, enum: [pdf, zip] } status: { type: string } ready: type: boolean description: Only true once the file exists and has not expired. download_url: type: [string, "null"] description: Null until ready. Never offer a link before then. message: type: string description: Written for a person to hear. # The personal tier's memorial shape. Deliberately NOT the business `Memorial` # above: this one carries the moderation verdict and the owner's own settings, # and omits organization concepts entirely. PersonalMemorial: type: object additionalProperties: false required: [id, slug, deceased_name, title, subtitle, obituary, birth_date, death_date, privacy, content_scope, is_published, contributions_enabled, manual_content_approval, hide_from_search, template_id, resting_place_url, resting_place_notes, moderation, is_deleted, recoverable_until, url, created_at] properties: id: { type: integer } slug: { type: string } deceased_name: { type: [string, "null"] } title: { type: [string, "null"] } subtitle: { type: [string, "null"] } obituary: { type: [string, "null"] } birth_date: { type: [string, "null"], format: date } death_date: { type: [string, "null"], format: date } privacy: { type: [string, "null"], enum: [public, restricted, private, closed, null] } content_scope: { type: [string, "null"] } is_published: { type: boolean } contributions_enabled: { type: boolean } manual_content_approval: { type: boolean } hide_from_search: { type: boolean } template_id: { type: [integer, "null"] } resting_place_url: { type: [string, "null"] } resting_place_notes: { type: [string, "null"] } moderation: type: object additionalProperties: false required: [status, reason, checked_at] properties: # `pending` means the safety check has not finished. The memorial is # NOT live yet — never report it as published on a pending status. status: { type: string, enum: [pending, passed, failed] } reason: { type: [string, "null"], description: "Why it was refused. Shown to the memorial's owner only." } checked_at: { type: [string, "null"] } is_deleted: { type: boolean } recoverable_until: { type: [string, "null"] } url: { type: [string, "null"], description: "The public page. Null while unpublished or deleted." } created_at: { type: [string, "null"] } Contribution: type: object additionalProperties: false required: [id, type, title, body, author, status, is_approved, is_private, is_pinned, is_hidden, moderation, can_approve, created_at] properties: id: { type: integer } type: { type: [string, "null"] } title: { type: [string, "null"] } body: { type: [string, "null"] } author: type: object additionalProperties: false required: [name, email, is_guest] properties: name: { type: [string, "null"] } email: { type: [string, "null"] } is_guest: { type: boolean } status: { type: string, enum: [pending, approved, rejected, hidden] } is_approved: { type: boolean } # Sent to the people who look after the memorial rather than to the # memorial page. Never publishable through this API: only the writer can # change it back, from the memorial itself. is_private: { type: boolean } is_pinned: { type: boolean } is_hidden: { type: boolean } moderation: type: object additionalProperties: false required: [status, reason] properties: status: { type: [string, "null"] } reason: { type: [string, "null"] } # Check this before offering to publish. False when the safety check # refused the tribute, or the writer has not confirmed their email. can_approve: { type: boolean } created_at: { type: [string, "null"] } DraftStatus: type: object required: [status] properties: status: { type: string, enum: [generating, ready, blocked] } id: { type: integer } reason: { type: [string, "null"] } message: { type: string } # The single, explicit memorial shape (list + by-id + by-slug). Every field # is specified; no undocumented properties. status: live = published; # awaiting_family = invited, waiting on the family; in_progress = being # written or generated. id is the live memorial id, null while pending. Memorial: type: object additionalProperties: false required: [id, slug, status, deceased_name, title, subtitle, description, birth_date, death_date, privacy, is_premium, web_url, created_at, updated_at] properties: id: { type: [integer, "null"] } slug: { type: string } status: { type: string, enum: [awaiting_family, in_progress, live] } deceased_name: { type: [string, "null"] } title: { type: [string, "null"] } subtitle: { type: [string, "null"] } description: { type: [string, "null"] } birth_date: { type: [string, "null"], format: date } death_date: { type: [string, "null"], format: date } privacy: { type: [string, "null"], enum: [public, restricted, private, closed, null] } is_premium: { type: boolean } web_url: { type: [string, "null"] } created_at: { type: [string, "null"] } updated_at: { type: [string, "null"] } ProvisionResult: type: object additionalProperties: false required: [id, status, deceased_name, organization_id, family, invitation_expires_at, created_at] properties: id: { type: integer } status: { type: string, enum: [awaiting_family, in_progress] } deceased_name: { type: [string, "null"] } organization_id: { type: [integer, "null"] } family: type: [object, "null"] additionalProperties: false required: [name, email] properties: name: { type: [string, "null"] } email: { type: [string, "null"] } invitation_expires_at: { type: [string, "null"] } created_at: { type: [string, "null"] } paths: /auth/device: post: summary: Start connecting (device flow) description: > Begin the RFC 8628 device authorization flow. Requires no credential — this is how an assistant obtains one. Show the person `verification_uri_complete`, ask them to open it and approve, then poll `/auth/token` with the `device_code` every `interval` seconds until it returns a token. Approving requires signing in, which is also how we verify the person's email address — so a memorial created with the resulting token is never asked to verify again. operationId: startDeviceAuthorization security: [] requestBody: required: false content: application/json: schema: type: object properties: client_name: type: string maxLength: 100 description: What to call your app on the approval screen, e.g. "Claude Desktop". responses: "201": description: Created content: application/json: schema: type: object additionalProperties: false required: [device_code, user_code, verification_uri, verification_uri_complete, expires_in, interval, message] properties: device_code: { type: string, description: "Secret. Send to /auth/token. Never show this to the person." } user_code: { type: string, description: "Short code the person reads or types, e.g. ABCD-2345." } verification_uri: { type: string } verification_uri_complete: { type: string, description: "Give the person this link — the code is already in it." } expires_in: { type: integer } interval: { type: integer, description: "Seconds to wait between polls." } message: { type: string } "429": description: Too many codes requested from this address /auth/token: post: summary: Collect the token description: > Exchange an approved `device_code` for a personal token. Poll no faster than `interval` seconds. Errors follow RFC 8628: `authorization_pending` means keep waiting, `slow_down` means you are polling too fast, `access_denied` means the person declined, `expired_token` means start again. The token is handed over once — store it. operationId: collectDeviceToken security: [] requestBody: required: true content: application/json: schema: type: object required: [device_code] properties: device_code: { type: string } responses: "200": description: Approved content: application/json: schema: type: object additionalProperties: false required: [access_token, token_type, scope] properties: access_token: { type: string } token_type: { type: string } scope: { type: string } "400": description: Not ready, declined, expired, or already collected content: application/json: schema: type: object required: [error, message] properties: error: type: string enum: [authorization_pending, slow_down, access_denied, expired_token, invalid_grant] error_description: { type: string } message: { type: string } /ping: get: summary: Ping description: Health/auth check — verifies your token and returns its granted abilities. operationId: ping responses: "200": description: OK content: application/json: schema: type: object additionalProperties: false required: [status, abilities, token] properties: status: { type: string } abilities: type: array items: { type: string } token: { type: [string, "null"] } "401": description: Missing or invalid token /organization: get: summary: Get your organization description: The profile, branding and memorial counts for the token's organization. operationId: getOrganization responses: "200": description: OK content: application/json: schema: type: object additionalProperties: false required: [id, name, subdomain, status, branding, memorials] properties: id: { type: integer } name: { type: string } subdomain: { type: [string, "null"] } status: { type: string } branding: type: object additionalProperties: false required: [display_name, white_label, brand_color, logo_url] properties: display_name: { type: [string, "null"] } white_label: { type: boolean } brand_color: { type: string } logo_url: { type: [string, "null"] } memorials: type: object additionalProperties: false required: [live, pending] properties: live: { type: integer } pending: { type: integer } /owners: get: summary: List memorial owners description: Owner name and email for each of your organization's memorials. operationId: listOwners responses: "200": description: OK content: application/json: schema: type: object additionalProperties: false required: [data] properties: data: type: array items: type: object additionalProperties: false required: [memorial_id, slug, role, owner] properties: memorial_id: { type: integer } slug: { type: string } role: { type: string } owner: type: object additionalProperties: false required: [name, email] properties: name: { type: [string, "null"] } email: { type: [string, "null"] } /memorials: get: summary: List your memorials description: > Every memorial scoped to your organization, newest first — live and pending (awaiting the family, or being written). Each carries a `status`. Filter by name with `q`. operationId: listMemorials parameters: - { in: query, name: q, required: false, description: "Filter by name (deceased or title).", schema: { type: string } } - { in: query, name: created_after, required: false, description: "Only memorials created on or after this date/time (inclusive).", schema: { type: string, format: date-time } } - { in: query, name: created_before, required: false, description: "Only memorials created on or before this date/time (inclusive).", schema: { type: string, format: date-time } } - { in: query, name: page, required: false, description: "1-based page number. Default 1.", schema: { type: integer, minimum: 1 } } - { in: query, name: per_page, required: false, description: "Items per page, 1–100. Default 25.", schema: { type: integer, minimum: 1, maximum: 100 } } responses: "200": description: OK content: application/json: schema: type: object additionalProperties: false required: [data, pagination] properties: data: type: array items: { $ref: "#/components/schemas/Memorial" } pagination: type: object additionalProperties: false required: [page, per_page, total, total_pages] properties: page: { type: integer } per_page: { type: integer } total: { type: integer } total_pages: { type: integer } "422": description: Validation error post: summary: Provision a memorial description: > Create a memorial for a family. With `action=invite` (default) we email the family a link to complete it. With `action=complete` your organization supplies the content and it goes live (processed asynchronously). operationId: provisionMemorial requestBody: required: true content: application/json: schema: type: object required: [deceased_name, designated_owner] properties: action: { type: string, enum: [invite, complete] } deceased_name: { type: string } birth_date: { type: string, format: date } death_date: { type: string, format: date } designated_owner: { type: string, enum: [family, organization, both] } privacy: { type: string, enum: [public, restricted, private, closed] } title: { type: string } subtitle: { type: string } obituary: { type: string } family_name: { type: string } family_email: { type: string, format: email } responses: "201": description: Family invited content: application/json: schema: { $ref: "#/components/schemas/ProvisionResult" } "202": description: Accepted — memorial is being created content: application/json: schema: { $ref: "#/components/schemas/ProvisionResult" } "422": description: Validation error /memorials/{memorial}: get: summary: Get a memorial description: > Fetch one memorial in your organization, by its numeric id (live memorials only) or by its slug (live or pending). operationId: getMemorial parameters: - in: path name: memorial required: true description: The numeric memorial id, or the memorial slug. schema: { type: string } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/Memorial" } "404": description: Not found, or outside your organization /memorials/{onboarding}/invitation: post: summary: Re-send the family invitation description: Rotate the claim link for an awaiting-family memorial and email it again. operationId: resendInvitation parameters: - { in: path, name: onboarding, required: true, schema: { type: integer } } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/ProvisionResult" } "404": description: No pending invitation for this memorial # --------------------------------------------------------------------------- # Personal tier. Every path below requires a personal token and acts only on # memorials the token's owner created. # --------------------------------------------------------------------------- /templates: get: summary: List the memorial designs description: > The designs a memorial can use, and the id to pass as `template_id`. Every design is available on every plan; there is no gating here. Designs are described by feel and tone only. The stored tags include faith and culture labels and those are deliberately withheld, because a design must never be presented to a family as being for a particular religion. operationId: listTemplates tags: [Memorials] responses: "200": description: The designs available. content: application/json: schema: type: object required: [data] properties: data: type: array items: type: object required: [id, name, description, feel] properties: id: { type: integer, description: "Pass this as template_id." } name: { type: string } description: { type: [string, "null"] } feel: type: array description: > Tone and audience, such as quiet-dignified or celebration-of-life. Never a religion. items: { type: string } "401": description: Missing or invalid token /me/memorials: get: summary: List the memorials you look after description: Memorials belonging to the connected account, newest first. operationId: listMyMemorials parameters: - { in: query, name: per_page, required: false, schema: { type: integer, minimum: 1, maximum: 100 } } - { in: query, name: page, required: false, schema: { type: integer, minimum: 1 } } responses: "200": description: OK content: application/json: schema: type: object required: [data] properties: data: type: array items: { $ref: "#/components/schemas/PersonalMemorial" } links: { type: object } meta: { type: object } post: summary: Create a memorial description: > Create a memorial for someone who has died, or for a pet. Only the name is required — ask for what the person can comfortably tell you rather than insisting on dates. Supply `obituary` and those words are kept verbatim; leave it out and one is drafted for the family to edit. Returns `202`, not `201`: the memorial is being prepared and safety-checked. Poll `poll_url` until it reports `ready` or `blocked`. Do not tell anyone their memorial is published before then. operationId: createMyMemorial requestBody: required: true content: application/json: schema: type: object required: [deceased_name] properties: deceased_name: { type: string, maxLength: 255 } title: { type: [string, "null"], maxLength: 255 } subtitle: { type: [string, "null"], maxLength: 255 } obituary: { type: [string, "null"], maxLength: 10000 } birth_date: { type: [string, "null"], format: date } death_date: { type: [string, "null"], format: date } responses: "202": description: Accepted — being prepared and safety-checked content: application/json: schema: type: object additionalProperties: false required: [id, status, deceased_name, slug, poll_url, message] properties: id: { type: integer } status: { type: string, enum: [generating] } deceased_name: { type: [string, "null"] } slug: { type: string } poll_url: { type: string } message: { type: string } "409": description: > You have reached a limit — either the number of memorials this account may look after at once, or how many may be created in a day. content: application/json: schema: type: object required: [code, message] properties: code: { type: string, enum: [live_cap_reached, daily_limit_reached, monthly_limit_reached] } message: { type: string } retry_after: { type: [integer, "null"] } "422": description: Validation error /me/memorials/{memorial}: get: summary: Get one of your memorials operationId: getMyMemorial parameters: - { in: path, name: memorial, required: true, schema: { type: integer } } responses: "200": description: OK content: application/json: schema: type: object required: [data] properties: data: { $ref: "#/components/schemas/PersonalMemorial" } "404": description: Not a memorial you look after patch: summary: Edit a memorial description: > Change a memorial's wording or settings. Personal tokens only. Send every change you intend to make in ONE request. Changes to wording (name, title, subtitle, obituary, resting-place notes) are safety-checked, and only one such change is accepted per memorial every five minutes — editing field by field will be refused with a `429` and a `Retry-After`. Settings like privacy, template and publication are free and unthrottled. A request that changes wording returns `202`: the memorial's moderation status goes back to `pending` until the check finishes. operationId: updateMemorial parameters: - { in: path, name: memorial, required: true, schema: { type: integer } } requestBody: required: true content: application/json: schema: type: object properties: deceased_name: { type: string, maxLength: 255 } title: { type: [string, "null"], maxLength: 255 } subtitle: { type: [string, "null"], maxLength: 255 } description: { type: [string, "null"], maxLength: 10000, description: "The obituary text." } resting_place_notes: { type: [string, "null"], maxLength: 2000 } resting_place_url: { type: [string, "null"], maxLength: 500, description: "A Google Maps or what3words link." } privacy: { type: string, enum: [public, restricted, private, closed] } content_scope: { type: string } is_published: { type: boolean } contributions_enabled: { type: boolean } manual_content_approval: { type: boolean } hide_from_search: { type: boolean } template_id: { type: integer } birth_date: { type: [string, "null"], format: date } death_date: { type: [string, "null"], format: date } responses: "200": description: Updated — nothing needing a safety check changed content: application/json: schema: { $ref: "#/components/schemas/PersonalMemorial" } "202": description: Updated — wording changed, safety check running content: application/json: schema: { $ref: "#/components/schemas/PersonalMemorial" } "404": description: Not a memorial you look after "422": description: Validation error "429": description: > Another wording change was made in the last five minutes. `Retry-After` gives the seconds to wait; other settings can still be changed meanwhile. content: application/json: schema: type: object required: [code, message, retry_after] properties: code: { type: string } message: { type: string } retry_after: { type: integer } delete: summary: Delete a memorial description: > Remove a memorial from view. It is never destroyed immediately — it stays recoverable from the owner's dashboard until `recoverable_until`. Confirm with the person before calling this. operationId: deleteMemorial parameters: - { in: path, name: memorial, required: true, schema: { type: integer } } responses: "200": description: Deleted content: application/json: schema: type: object additionalProperties: false required: [id, deleted, recoverable_until, message, quota] properties: id: { type: integer } deleted: { type: boolean } recoverable_until: { type: [string, "null"] } message: { type: string } quota: type: object additionalProperties: false required: [live_used, live_cap, live_remaining] properties: live_used: { type: integer } live_cap: { type: integer } live_remaining: { type: integer } "404": description: Not a memorial you look after /me/memorials/{memorial}/status: get: summary: Check a memorial's safety-check status description: > Whether the automated safety review has finished for this memorial, and whether it passed. Poll this after creating or rewording one. operationId: getMemorialStatus parameters: - { in: path, name: memorial, required: true, schema: { type: integer } } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/PersonalMemorial" } "404": description: Not a memorial you look after /me/drafts/{onboarding}: get: summary: Check a memorial being created description: > Poll a memorial that is still being prepared. `generating` means keep waiting; `ready` means it is live; `blocked` means the safety check refused it and `reason` says why, so you can suggest a rewording. operationId: getDraftStatus parameters: - { in: path, name: onboarding, required: true, schema: { type: integer } } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/DraftStatus" } "404": description: Not a draft you created /me/memorials/{memorial}/visits: get: summary: How many people have visited description: > Counts of visits, never of visitors: nothing is recorded about who came. Free on every tier. operationId: showVisits tags: [Memorials] parameters: - { name: memorial, in: path, required: true, schema: { type: integer } } responses: "200": description: The counts. content: application/json: schema: type: object properties: today: { type: integer } last_24_hours: { type: integer } last_7_days: { type: integer } last_30_days: { type: integer } last_year: { type: integer } total: { type: integer } message: { type: string } "401": { description: "Missing or invalid token" } /me/memorials/{memorial}/fund: get: summary: The memorial fund, if there is one operationId: showFund tags: [Memorials] parameters: - { name: memorial, in: path, required: true, schema: { type: integer } } responses: "200": { description: "The fund, or exists=false." } "401": { description: "Missing or invalid token" } put: summary: Set the memorial fund description: > A short message and up to two links, asking people to give somewhere instead of sending flowers. **Premium.** The wording is safety-checked, so this returns `202` and the fund is not showing until the check passes. A heading with no URL behind it is dropped, since it would label nothing. operationId: setFund tags: [Memorials] parameters: - { name: memorial, in: path, required: true, schema: { type: integer } } requestBody: required: true content: application/json: schema: type: object required: [message] properties: message: { type: string, maxLength: 2000 } link_one_title: { type: string, maxLength: 80 } link_one_url: { type: string, format: uri } link_two_title: { type: string, maxLength: 80 } link_two_url: { type: string, format: uri } responses: "202": { description: "Saved, and being safety-checked." } "401": { description: "Missing or invalid token" } "422": { description: "Premium is needed, or the wording failed validation." } delete: summary: Remove the memorial fund operationId: deleteFund tags: [Memorials] parameters: - { name: memorial, in: path, required: true, schema: { type: integer } } responses: "200": { description: "Removed." } "401": { description: "Missing or invalid token" } /me/memorials/{memorial}/broadcasts: get: summary: Announcements and events already posted operationId: listBroadcasts tags: [Memorials] parameters: - { name: memorial, in: path, required: true, schema: { type: integer } } responses: "200": { description: "What has been posted, and whether each is showing." } "401": { description: "Missing or invalid token" } post: summary: Post an announcement or an event description: > The family telling everyone who follows the memorial something, such as when the service is. **Premium.** Safety-checked before it appears, so this returns `202` and `is_showing` is false until the check passes. **Nobody is emailed from here**, whatever is sent: an assistant does not send messages to people. operationId: postBroadcast tags: [Memorials] parameters: - { name: memorial, in: path, required: true, schema: { type: integer } } requestBody: required: true content: application/json: schema: type: object required: [title] properties: kind: { type: string, enum: [announcement, event], default: announcement } title: { type: string, maxLength: 255 } content: { type: string, maxLength: 10000 } location: { type: string, description: "Events only." } starts_at: { type: string, format: date-time, description: "Events only, and required for them." } ends_at: { type: string, format: date-time, description: "Events only." } rsvp: { type: string, description: "Events only. How to reply, if not through the memorial." } rsvp_enabled: { type: boolean, description: "Events only." } responses: "202": { description: "Posted, and being safety-checked." } "401": { description: "Missing or invalid token" } "422": { description: "Premium is needed, or a required field is missing." } /me/events/{event}/rsvps: get: summary: Who is coming description: > Replies to an event. `counts.people` is guests expected, not replies received: a yes for a party of four is four. These are names people gave the family in confidence. They belong to the owner and nowhere else. operationId: listRsvps tags: [Memorials] parameters: - { name: event, in: path, required: true, schema: { type: integer } } responses: "200": { description: "The replies and the counts." } "401": { description: "Missing or invalid token" } /me/memorials/{memorial}/share-link: post: summary: Get a link that grants access description: > A memorial's canonical `url` grants nobody access when it is private, so sharing that alone hands a family a link that looks right and does not work. This returns one that works. **The link is a bearer credential.** Whoever opens it gets the role it carries, and links get forwarded. Ask for the least access that does the job. Reuses the existing link for a role unless `fresh` is set, so a link the family already has is not silently broken. operationId: createShareLink tags: [Memorials] parameters: - name: memorial in: path required: true schema: { type: integer } requestBody: required: true content: application/json: schema: type: object required: [role] properties: role: type: string enum: [viewer, contributor, moderator, owner] description: > What the link grants. Not every role is available on every memorial: a viewer link on a public memorial grants nothing new, so it is refused. fresh: type: boolean description: Replace the existing link, stopping the old one working. responses: "200": description: The link. content: application/json: schema: type: object required: [role, url, grants, message] properties: role: { type: string } url: { type: string } grants: { type: string } message: { type: string } "401": description: Missing or invalid token "422": description: That role is not available for this memorial. /me/memorials/{memorial}/exports: post: summary: Make the Memorial Book, or the family's own copy description: > Starts a PDF Memorial Book (`pdf`, Premium) or an archive of everything gathered (`zip`, free for every owner as a data-access download). Generation is queued, so this returns `202` with a status and no download link. Poll the export until `ready` is true. **Never present a download before then.** operationId: createExport tags: [Memorials] parameters: - name: memorial in: path required: true schema: { type: integer } requestBody: content: application/json: schema: type: object properties: type: { type: string, enum: [pdf, zip], default: pdf } responses: "200": description: One is already being prepared. content: application/json: schema: { $ref: "#/components/schemas/ExportState" } "202": description: Started. content: application/json: schema: { $ref: "#/components/schemas/ExportState" } "401": description: Missing or invalid token "422": description: The Memorial Book needs Premium. /me/memorials/{memorial}/exports/{export}: get: summary: How an export is getting on operationId: showExport tags: [Memorials] parameters: - name: memorial in: path required: true schema: { type: integer } - name: export in: path required: true schema: { type: integer } responses: "200": description: Its state. content: application/json: schema: { $ref: "#/components/schemas/ExportState" } "401": description: Missing or invalid token /me/memorials/{memorial}/media: post: summary: Get a link for uploading a cover photo description: > Returns a short-lived signed URL. `PUT` the JPEG to it as multipart form-data in the `file` field. Do not send images as base64 — upload the bytes directly to the returned URL. operationId: createMemorialUpload parameters: - { in: path, name: memorial, required: true, schema: { type: integer } } responses: "201": description: Created content: application/json: schema: type: object additionalProperties: false required: [upload_url, expires_at, method, field, accepts, max_bytes, message] properties: upload_url: { type: string } expires_at: { type: string } method: { type: string } field: { type: string } accepts: { type: array, items: { type: string } } max_bytes: { type: integer } message: { type: string } "404": description: Not a memorial you look after /memorials/{memorial}/contributions: get: summary: Read tributes left on a memorial description: > What people have written on one of your memorials. Defaults to those awaiting your decision; use `status` for the others. Check `can_approve` before offering to publish something — a tribute the safety check refused, or one whose writer has not confirmed their email, cannot be published. operationId: listContributions parameters: - { in: path, name: memorial, required: true, schema: { type: integer } } - { in: query, name: status, required: false, schema: { type: string, enum: [pending, approved, rejected, deleted] } } - { in: query, name: per_page, required: false, schema: { type: integer, minimum: 1, maximum: 100 } } responses: "200": description: OK content: application/json: schema: type: object required: [data] properties: data: type: array items: { $ref: "#/components/schemas/Contribution" } links: { type: object } meta: { type: object } "404": description: Not a memorial you look after /contributions/{contribution}/approve: post: summary: Publish a tribute description: > Publish a tribute left on one of your memorials. Refused for tributes the safety check flagged, and for those still awaiting the writer's email confirmation — the same two limits that apply on the website. operationId: approveContribution parameters: - { in: path, name: contribution, required: true, schema: { type: integer } } responses: "200": description: Published content: application/json: schema: { $ref: "#/components/schemas/Contribution" } "404": description: Not on a memorial you look after "422": description: Cannot be published content: application/json: schema: type: object required: [code, message] properties: code: { type: string, enum: [blocked_by_safety_check, awaiting_verification] } message: { type: string } /contributions/{contribution}: delete: summary: Hide a tribute description: > Hide a tribute from the memorial. Recoverable — it can be restored. Because this concerns something a grieving person wrote, confirm with the memorial's owner before doing it. operationId: hideContribution parameters: - { in: path, name: contribution, required: true, schema: { type: integer } } responses: "200": description: Hidden content: application/json: schema: type: object required: [id, hidden, message] properties: id: { type: integer } hidden: { type: boolean } message: { type: string } "404": description: Not on a memorial you look after /contributions/{contribution}/reject: post: summary: Reject a tribute description: Mark a tribute as rejected so it stays off the memorial, optionally recording why. operationId: rejectContribution parameters: - { in: path, name: contribution, required: true, schema: { type: integer } } requestBody: required: false content: application/json: schema: type: object properties: reason: { type: string, maxLength: 500 } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/Contribution" } "404": description: Not on a memorial you look after /contributions/{contribution}/pin: post: summary: Pin a tribute to the top description: > Puts a tribute in the pinned band at the top of the memorial. It must be approved first: pinning something still being safety-checked would place it at the top the moment it passed, without anyone choosing that. operationId: pinContribution tags: [Contributions] parameters: - { name: contribution, in: path, required: true, schema: { type: integer } } responses: "200": { description: "Pinned." } "401": { description: "Missing or invalid token" } "422": { description: "Not approved yet." } /contributions/{contribution}/unpin: post: summary: Unpin a tribute description: Moves it back to its usual place. Nothing is removed. operationId: unpinContribution tags: [Contributions] parameters: - { name: contribution, in: path, required: true, schema: { type: integer } } responses: "200": { description: "Unpinned." } "401": { description: "Missing or invalid token" } /contributions/{contribution}/restore: post: summary: Restore a hidden tribute description: Put a hidden tribute back on the memorial. operationId: restoreContribution parameters: - { in: path, name: contribution, required: true, schema: { type: integer } } responses: "200": description: OK content: application/json: schema: { $ref: "#/components/schemas/Contribution" } "404": description: Not on a memorial you look after