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
POST /v1/occasionsrequirespersonId(the person it's for) andtimezone(IANA, e.g.America/New_York— every reminder for this occasion fires relative to it). Default new occasions toreminder_only— themodefield onPOST /v1/occasionsis one ofreminder_only(just a reminder; the user chooses what to send each time) orapproval_required(a draft is prepared for the user to explicitly approve before payment). Amuse_candidateremains inactive until the user explicitly confirms it. When creating an occasion viaPOST /v1/occasions, always passkindexplicitly — it defaults toother(safe: no annual reminder), notbirthday. Onlybirthday,anniversary,christmas, andeasterrecur annually;otheris one-off by default (passrecurring: trueto opt anotheroccasion into the annual cycle).- Poll
POST /v1/reminders/claimfrom a daily scheduled task. Say nothing when the returned list is empty. - Suggest a physical gift at most once per reminder. Call
GET /v1/occasions/{id}/fulfillment-choicesand 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. - Flowers and gifts both browse Goody's real catalogue via the connector —
GET /v1/goody/productslists 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-quotereturnsserviceFee/totalas 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-draftcreates the provider-priced draft to pay — sameproductId/addressas the quote, plusmessageand optionalvariantIds.
Addresses
- Before quoting any gift, flower, or postcard, check
GET /v1/people/{id}'saddressConfirmedAtfirst. 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'snull(a new person, or the address was recently edited viaPATCH /v1/people/{id}), read the recipient's full delivery address back to the user and get explicit confirmation. Either way, sendaddressConfirmed: truewith the quote/draft/selection request — omitaddressto use the person's stored one, or sendaddress: {line1, city, state, postalCode, country, ...}for a one-off address that shouldn't replace what's on file (a one-off address doesn't affectaddressConfirmedAteither way).stateis 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 byGET /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 withPOST /v1/people(name, andaddressif known up front). If a person's stored name or address turns out wrong or incomplete, correct it withPATCH /v1/people/{id}rather than creating a new person — existing occasions and reminders keep pointing at the same, now-corrected record. - Before the account holder's first postcard, check
GET /v1/sender-addressfor 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 withPUT /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
- Before asking about size or themes, check
GET /v1/people/{id}/last-postcard—nullfor a person with no prior paid postcard, otherwise their last one'spostcardSize,template, andmessage. 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'snull, 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. - 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-sizesfor the three options (6x4,9x6,11x6), each with dimensions, a message-length limit, and price including its delta vs the6x4base. Suggest6x4as the default, with the other two as one-tap alternatives. Pass the chosen id aspostcardSizelater, when calling/v1/postcard-options/{id}/select(or/v1/draftsdirectly); omitting it defaults to6x4. If the user has already said they want to send one specific photo of their own — not choose between AI-generated concepts — skip straight toPOST /v1/draftswithtemplate: "photo", that photo'simageUrl, anduserPhoto: true(plus an optionalcaption) instead of the theme/four-fronts flow below: there's nothing to pick between when it's already one specific photo. - 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. - 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. - 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/imagesas multipart fieldimage; the returned 24-hourimageUrlis served byte-for-byte byGET /v1/images/{id}. Use that URL inPOST /v1/postcard-options(occasionIdplus the fouroptions), passing any ticked theme ids asthemes. 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. - 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}/customizewith its returnedimageUrland acustomizationPromptdescribing 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 passuserPhoto: 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 ascaption; it renders into the polaroid's blank space below the photo. WithoutuserPhoto, the image is used as-is with no mat, same as before.
Postcards: back message, preview, and pricing
{id}in every/v1/postcard-options/{id}/...call below (preview,customize,select) is the individual option's own id — one entry'sidfrom theoptionsarray, not the option set's own top-levelid. After the user selects a front, show an actual rendered preview alongside the question, not just quoted text: fetchGET /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, defaultleft) andtextColor(a hex string, default#241b17) can also be edited if the user wants to change them. Once confirmed, call/v1/postcard-options/{id}/selectwith thepostcardSizechosen earlier, theaddressfrom earlier, and any editedmessage/font/fontSizePtto create the priced draft. If the message doesn't actually fit the card at that font size, both calls reject with a400naming 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.- Show an actual rendered preview, not just a text summary: for a postcard, fetch
GET /v1/drafts/{id}/previews/front.jpgand.../back.jpg(300 DPI;back.jpgrenders the user's real message, font, and layout;front.jpgis 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.usdEquivalentandprice.fxRateshow 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. - 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 sendexpress: trueif 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 a400naming the countries currently supported. The resulting draft'smailingClassandprice.expressconfirm which service and surcharge actually applied.
Payment
- Call
POST /v1/drafts/{id}/payonly after explicit user approval, withidempotencyKey(a unique string per payment attempt — retries reuse the same value, never a fresh one) andpaymentMethod. Onlystate: confirmedwith a returnedordermeans fulfilment was actually ordered. Astate: failedresponse includes areasonstring — read it back to the user (e.g. a decline) rather than saying payment "failed" with no explanation.paymentMethodmust be one of Stripe's own test-mode payment method IDs —pm_card_visafor a payment that succeeds,pm_card_chargeDeclinedfor 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, omitpaymentMethodentirely — Dearly responds402with a payment challenge the agent pays and retries against this same endpoint;idempotencyKeycan be omitted too, falling back to the payment's own receipt reference. If the agent can't pay an MPP challenge, acheckoutUrl/checkoutExpiresAtpair is included directly in that same402response body'smetaobject — present that link to the user to pay with their own card in a browser, then pollGET /v1/orders/{id}/statusonce fulfilment has actually fired, rather than treating opening the link as completion. When no MPP challenge is offered at all, the same omitted-paymentMethodrequest instead returns200with{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 toPOST /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. - 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), callPATCH /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
GET /v1/orderslists 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 includespersonName/occasionKind/productType/messageso 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.POST /v1/orders/{id}/cancelis 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). A400means the order genuinely can no longer be cancelled — not a transient failure to retry.- Never say "sent" for an order in the
placedstate — say "ordered". Only say "sent" once a provider callback recordsshipped.GET /v1/orders/{id}/statusrefreshes an order from its provider and returnsproviderStatus(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. PollGET /v1/action-itemsalongsidePOST /v1/reminders/claimand relay anyorder_status_changeditem 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
GET /v1/api-keys,GET /v1/sender-address,GET /v1/people/{id},GET /v1/people/{id}/last-postcard,GET /v1/occasions/{id}/fulfillment-choices,GET /v1/goody/products,GET /v1/goody/products/{id},POST /v1/occasions/{id}/goody-quote(computes a price range only; nothing is persisted),GET /v1/drafts/{id}/previews/{side}.jpg,GET /v1/images/{id},GET /v1/postcard-themes,GET /v1/postcard-countries,GET /v1/postcard-sizes,GET /v1/postcard-options/{id},GET /v1/postcard-options/{id}/preview/{side}.jpg,POST /v1/postcard-options/{id}/preview/{side}.jpg(aPOSTonly so personal data in the request body never lands in a URL; it persists nothing, same as theGETform),GET /v1/orders,GET /v1/orders/{id}/preview,GET /v1/action-items,GET /v1/funnel,GET /v1/data-export.
Write (not sensitive)
DELETE /v1/api-keys/{id}(revoking access reduces risk, not grants it),PUT /v1/sender-address,POST /v1/people,PATCH /v1/people/{id},POST /v1/occasions,PATCH /v1/occasions/{id},POST /v1/reminders/claim(internal bookkeeping — marks reminders claimed so they aren't sent twice),POST /v1/drafts,POST /v1/occasions/{id}/goody-draft,POST /v1/images,POST /v1/postcard-options,POST /v1/postcard-options/{id}/customize,POST /v1/postcard-options/{id}/select(all five of these create a priced draft/option but never charge anything).GET /v1/orders/{id}/status— reads as a routine status check, but is classified Write, not Read, because of a real side effect: it persists the freshly-fetched provider status, and if the provider independently reports an order as cancelled, it automatically issues a refund to the original payment method with no separate approval step. This never charges the user anything — it only ever returns money already paid, triggered by the provider's own cancellation, not by Muse's or the user's request — so it is not classified Sensitive, but it is disclosed here explicitly rather than left to look like a side-effect-free Read.
Sensitive write
POST /v1/api-keys— mints a new, durable credential with full access to the connected account.POST /v1/drafts/{id}/pay— a real charge and a real physical order; always requires the explicit per-attempt approval described in the Payment section above.POST /v1/orders/{id}/cancel— irreversibly cancels a real order with the provider and issues a refund.DELETE /v1/account— irreversibly erases every record tied to the account, described in full under Access requirements below.
Access requirements
- Postcard recipients: US, Canada, UK, and Australia (see
GET /v1/postcard-countriesfor the full list). Flowers and gifts (via Goody) remain US-only. Postcard quotes are priced in the recipient's local currency (fixed conversion rates, not live FX) with a USD equivalent alongside; gift/flower quotes stay USD. - Payment, postcard printing, and gift fulfilment all run in live mode — a completed order is a real charge and a real physical item sent to the recipient, not a sandboxed simulation.
- Access is currently issued directly by the operator (no public self-service signup).
- If the user asks to see or export everything Dearly has stored about them,
GET /v1/data-exportreturns it all. - If the user explicitly asks to delete their data or close their account (not implied by an unrelated complaint — only act on a clear, direct request), first offer
GET /v1/data-exportso they can get a copy before anything is removed, then confirm plainly what deletion actually does before calling it:DELETE /v1/accountimmediately and permanently erases every person, occasion, reminder, draft, order, payment record, and sender address tied to the account, and revokes the API key — there is no undo, and nothing further can be done with that account afterward. This only covers Dearly's own records: it does not recall a postcard already printed/mailed or a gift already shipped, and does not reach into Stripe's, PostGrid's, or Goody's own independent records, which are each governed by their own retention policies. Only call the endpoint after the user confirms they understand and still want to proceed.