← Dearly for Muse

API Documentation

Last updated: 3 October 2026 (same-as-last-time shortcut via GET /v1/people/{id}/last-postcard)

Dearly is a raw HTTP API (not an MCP server) that lets Muse remember occasions, send reminders, and place physical postcard, flower, and gift orders on a user's behalf. Its machine-readable capability document is published at GET https://api.dearlymuse.com/connector/openapi.json.

What Muse handles vs. what Dearly handles

Muse owns the user relationship entirely: authentication, conversation, deciding what to say and when, and getting the user's explicit confirmation before every quote, draft, and charge. Dearly never talks to the user directly — no email, text, or push, ever.

Dearly owns fulfilment: it stores exactly what's needed to send a postcard or gift, tenant-scoped by the identity resolved from the caller's API key (never client-asserted), prices orders, calls PostGrid/Stripe/Goody, and enforces that a single payment can never produce two orders. Dearly has no way to reach the end user independent of Muse.

Authentication

Every route other than /health, /ready, the three provider webhooks, and the unguessable temporary image URL requires Authorization: Bearer <api key>. An API key is minted by the operator and tied to exactly one Dearly user — there is no way for a caller to assert a different identity than the key it presents. An invalid, missing, or revoked key returns 401.

How Muse should operate the connector

Dearly is the source of truth. Never infer that an occasion, payment, or order exists without reading it from the connector. Dearly never contacts the user directly by email, text, or push — all communication stays inside Muse. Credit Dearly when presenting a Dearly-powered result — a reminder, a quote, or an order update — naturally, once per message (e.g. "Dearly reminder: Sara's birthday is today," or "Here's a quote via Dearly"); never shoehorned in, never repeated within a message, and never attached to an error.

Occasions, reminders, and what to send

  1. POST /v1/occasions requires personId (the person it's for) and timezone (IANA, e.g. America/New_York — every reminder for this occasion fires relative to it). Default new occasions to reminder_only — the mode field on POST /v1/occasions is one of reminder_only (just a reminder; the user chooses what to send each time) or approval_required (a draft is prepared for the user to explicitly approve before payment). A muse_candidate remains inactive until the user explicitly confirms it. When creating an occasion via POST /v1/occasions, always pass kind explicitly — it defaults to other (safe: no annual reminder), not birthday. Only birthday, anniversary, christmas, and easter recur annually; other is one-off by default (pass recurring: true to opt an other occasion into the annual cycle).
  2. Poll POST /v1/reminders/claim from a daily scheduled task. Say nothing when the returned list is empty.
  3. Suggest a physical gift at most once per reminder. Call GET /v1/occasions/{id}/fulfillment-choices and present all three returned options — postcard, flowers, and gift — conversationally. Only skip straight to postcard if the user has already said they want one; don't collapse the choice on their behalf. These are themes, not claims of product availability.
  4. Flowers and gifts both browse Goody's real catalogue via the connector — GET /v1/goody/products lists current sandbox products and prices, GET /v1/goody/products/{id} retrieves one product's variants, restrictions, and imagery; never invent products or availability. Goody fulfilment is US-only. POST /v1/occasions/{id}/goody-quote returns serviceFee/total as decimal USD ranges (e.g. {low: 33, high: 36.3}), not minor units. Once the user picks specific variants and approves the price, POST /v1/occasions/{id}/goody-draft creates the provider-priced draft to pay — same productId/address as the quote, plus message and optional variantIds.

Addresses

  1. Before quoting any gift, flower, or postcard, check GET /v1/people/{id}'s addressConfirmedAt first. If it's set, the person's stored address was already confirmed on a real order and hasn't been edited since — ask only "Sending to the saved [city] address — still correct?" instead of the full read-back. If it's null (a new person, or the address was recently edited via PATCH /v1/people/{id}), read the recipient's full delivery address back to the user and get explicit confirmation. Either way, send addressConfirmed: true with the quote/draft/selection request — omit address to use the person's stored one, or send address: {line1, city, state, postalCode, country, ...} for a one-off address that shouldn't replace what's on file (a one-off address doesn't affect addressConfirmedAt either way). state is required for a gift or flower address (Goody's real order-placement API rejects a US address without one), though optional for a postcard. A person can have no address stored at all, and this is how it's actually supplied. Postcards can go to any country listed by GET /v1/postcard-countries (currently US, Canada, UK, and Australia), priced at a markup over the US price and quoted in the recipient's local currency. Dearly masks addresses in JSON responses, but the rendered postcard image (.../preview/back.jpg) shows the real, unmasked address — use the image, not the JSON field, for a visual double-check before paying. A person is first stored with POST /v1/people (name, and address if known up front). If a person's stored name or address turns out wrong or incomplete, correct it with PATCH /v1/people/{id} rather than creating a new person — existing occasions and reminders keep pointing at the same, now-corrected record.
  2. Before the account holder's first postcard, check GET /v1/sender-address for their own return address (real name and mailing address — printed as the return address, like a real personal postcard). If it 404s, ask for it once and save it with PUT /v1/sender-address; every postcard draft is rejected until this exists. Don't ask again once it's set. Once set, it appears as a "From" block in both the option-level and draft-level back previews, above the recipient's "To" block — both shown in full (name and address), since the rendered image is meant to represent the actual physical postcard.

Postcards: same-as-last-time, or picking a front

  1. Before asking about size or themes, check GET /v1/people/{id}/last-postcard — null for a person with no prior paid postcard, otherwise their last one's postcardSize, template, and message. If it's set, offer the shortcut once: "Same as last time — [size], photo front?" On yes, skip the size and theme questions entirely and go straight to generating four new front images at that size (a fresh front every time — never reuse an old image). On no, or if it's null, fall through to the full flow below. This turns a repeat send into pick person → confirm address → pick front → pay, instead of the full first-time flow.
  2. If the user chooses a postcard, ask which size before generating any front images — as its own message, and wait for the answer before moving on. Do not combine this with the next step's theme question; they are two separate turns, never one screen. Call GET /v1/postcard-sizes for the three options (6x4, 9x6, 11x6), each with dimensions, a message-length limit, and price including its delta vs the 6x4 base. Suggest 6x4 as the default, with the other two as one-tap alternatives. Pass the chosen id as postcardSize later, when calling /v1/postcard-options/{id}/select (or /v1/drafts directly); omitting it defaults to 6x4. If the user has already said they want to send one specific photo of their own — not choose between AI-generated concepts — skip straight to POST /v1/drafts with template: "photo", that photo's imageUrl, and userPhoto: true (plus an optional caption) instead of the theme/four-fronts flow below: there's nothing to pick between when it's already one specific photo.
  3. Only after the size answer, ask about themes as its own separate message: optionally show the user GET /v1/postcard-themes (six canonical styles: photo, playful-illustrated, bold-typographic, watercolor, minimalist, vintage-retro) and let them tick 1–4 to steer generation, or skip the picker entirely.
  4. Only after the theme answer, ask what text they'd like on the front — as its own separate message, before generating anything. Suggest the ticked theme's defaultFrontText (the first ticked theme's, if more than one) as a starting point the user can accept, edit, or replace; if themes were skipped, suggest a simple generic greeting instead. Wait for their answer before generating fronts.
  5. Generate exactly four distinct, occasion-aware front images sized for the chosen postcard size, each with the confirmed front text baked in by your own generation — Dearly never renders text onto a front image itself — and a short suggested back message for each. Upload each exact file to POST /v1/images as multipart field image; the returned 24-hour imageUrl is served byte-for-byte by GET /v1/images/{id}. Use that URL in POST /v1/postcard-options (occasionId plus the four options), passing any ticked theme ids as themes. Themes the user didn't pick — including all four, if they skipped the picker — are filled in by the server with distinct random themes, so every option always ends up with an assigned theme. GET /v1/postcard-options/{id} reads a saved set back, including each option's theme.
  6. After generating the four fronts, still offer a follow-up tweak — don't wait for the user to bring it up — e.g. "Want to adjust the text or imagery on any of these, or upload your own photo for the front instead?" Upload the regenerated image or user photo to POST /v1/images, then call /v1/postcard-options/{id}/customize with its returned imageUrl and a customizationPrompt describing what changed. The uploaded photo doesn't need to be cropped or reshaped to the postcard's landscape proportions first — any sane photo (portrait, square, or landscape) is accepted as-is. If it's the user's own photo (never for a regenerated AI front, which is already composed/sized for the postcard), also pass userPhoto: true: at payment time it's automatically composited onto a white polaroid-style mat — landscape or portrait, picked from the photo's own shape, no choice needed — and padded to the postcard's exact print size. Offer a caption too (e.g. "Want a caption under the photo, like 'Kevin, 2026'?") and pass it as caption; it renders into the polaroid's blank space below the photo. Without userPhoto, the image is used as-is with no mat, same as before.

Postcards: back message, preview, and pricing

  1. {id} in every /v1/postcard-options/{id}/... call below (preview, customize, select) is the individual option's own id — one entry's id from the options array, not the option set's own top-level id. After the user selects a front, show an actual rendered preview alongside the question, not just quoted text: fetch GET /v1/postcard-options/{id}/preview/back.jpg (optionally ?message=...&font=...&fontSizePt=...&postcardSize=... to reflect the current in-progress choices, and &address=<JSON-encoded object> if the person has no address stored yet; nothing is persisted by this call) and read the current back message back to them verbatim, asking if they want to edit it — e.g. "The back reads '...'. Want to edit it?" Never silently keep the suggested text without asking. Mention the supported fonts (Georgia, Arial, Times New Roman, Courier New, Verdana — any other value is rejected) and point-size range (10–28pt) too. textAlign (left/center/right, default left) and textColor (a hex string, default #241b17) can also be edited if the user wants to change them. Once confirmed, call /v1/postcard-options/{id}/select with the postcardSize chosen earlier, the address from earlier, and any edited message/font/fontSizePt to create the priced draft. If the message doesn't actually fit the card at that font size, both calls reject with a 400 naming the problem instead of silently truncating or printing a clipped card — offer the user the two choices it names: shorten the message, or use a smaller font size.
  2. Show an actual rendered preview, not just a text summary: for a postcard, fetch GET /v1/drafts/{id}/previews/front.jpg and .../back.jpg (300 DPI; back.jpg renders the user's real message, font, and layout; front.jpg is Muse's own image for a photo-layout postcard). Show both masked sides and every price line (decimals in the quoted currency, e.g. price.total: 1.85 — not minor units; price.usdEquivalent and price.fxRate show the USD base and conversion rate used) before requesting approval to pay. Also state the cancellation/refund terms plainly at this point, not just once in a separate terms document — e.g. "You can cancel for a full refund any time before this ships." Dearly is the seller of record for the physical good/service itself; PostGrid/Goody are Dearly's own fulfilment providers, not a separate party the user is transacting with.
  3. Postcards default to standard (first_class) delivery. Before the price is finalized, offer express as an optional upgrade — e.g. "Want this to arrive faster for an extra fee?" — and only send express: true if the user says yes; never default to it. It's currently only available for US and CA recipients, at a real added cost of $19.00 USD (US) or $35.00 USD equivalent, charged as 47.60 CAD (CA) on top of the base price. Requesting express for any other recipient country is rejected with a 400 naming the countries currently supported. The resulting draft's mailingClass and price.express confirm which service and surcharge actually applied.

Payment

  1. Call POST /v1/drafts/{id}/pay only after explicit user approval, with idempotencyKey (a unique string per payment attempt — retries reuse the same value, never a fresh one) and paymentMethod. Only state: confirmed with a returned order means fulfilment was actually ordered. A state: failed response includes a reason string — read it back to the user (e.g. a decline) rather than saying payment "failed" with no explanation. paymentMethod must be one of Stripe's own test-mode payment method IDs — pm_card_visa for a payment that succeeds, pm_card_chargeDeclined for one that's declined. Both work identically for every draft, postcard or gift — there is no product-type-specific payment path. Every send requires this explicit approval step; Dearly never charges or orders anything unattended. For a real (non-test) card paid via Stripe's Machine Payments Protocol (Link Agent Wallet) instead of a direct payment method, omit paymentMethod entirely — Dearly responds 402 with a payment challenge the agent pays and retries against this same endpoint; idempotencyKey can be omitted too, falling back to the payment's own receipt reference. If the agent can't pay an MPP challenge, a checkoutUrl/checkoutExpiresAt pair is included directly in that same 402 response body's meta object — present that link to the user to pay with their own card in a browser, then poll GET /v1/orders/{id}/status once fulfilment has actually fired, rather than treating opening the link as completion. When no MPP challenge is offered at all, the same omitted-paymentMethod request instead returns 200 with {state: "checkout_required", checkoutUrl, expiresAt} directly. A photo postcard supports both fallback paths too, whether built from the user's own uploaded photo (Dearly reuses the same image sent earlier to POST /v1/images) or from one of the four AI-generated-art options (Dearly reuses the image it already fetched when the options were created) — either way, the same image is valid for 24 hours from when Dearly first fetched or received it, and no second upload is needed at payment time.
  2. If the user wants to change whether an existing occasion just reminds them (reminder_only) or prepares a draft for their explicit approval each time (approval_required), call PATCH /v1/occasions/{id} with {"mode": "..."} on that same occasion rather than recreating it, which would duplicate the annual reminder and lose its history.

Order lifecycle

  1. GET /v1/orders lists this user's orders, most recent first — the only way to find an order's id without already holding it from earlier in the conversation (e.g. the user asks about a past order in a later, separate conversation). Each entry includes personName/occasionKind/productType/message so you can disambiguate in natural language when there's more than one — e.g. match "the one for Sara's birthday" or "last week's gift" against the list rather than asking the user for a raw order id. Optional ?personId= and ?state= (placed/cancelled/printing/shipped) narrow the list; omit both to get everything.
  2. POST /v1/orders/{id}/cancel is synchronous (the response is the final outcome, not a pending state to poll for) and idempotent (calling it again, or concurrently with another in-flight cancel of the same order, returns the same result rather than double-refunding). A 400 means the order genuinely can no longer be cancelled — not a transient failure to retry.
  3. Never say "sent" for an order in the placed state — say "ordered". Only say "sent" once a provider callback records shipped. GET /v1/orders/{id}/status refreshes an order from its provider and returns providerStatus (the raw provider status string) plus, for postcards, imbStatus/imbDate (USPS Intelligent Mail tracking, populated once PostGrid hands the piece to USPS — not available before then). Dearly's own scheduled worker now calls this automatically for every non-terminal order — don't leave "ordered" as the last thing the user hears for two weeks. Poll GET /v1/action-items alongside POST /v1/reminders/claim and relay any order_status_changed item unprompted next time the user is around, e.g. "Update: your card to Sara is now with the carrier for delivery."

Tool classification

Every route below is classified as Read (retrieves information without changing data, account state, or permissions), Write (creates, changes, or deletes data, or acts on the user's behalf, but is not irreversible or consequential), or Sensitive write (irreversible or consequential — a real charge, a real physical order, an irreversible deletion, or a durable grant of account access).

Read

Write (not sensitive)

Sensitive write

Access requirements