Daymaker Partner API
Send a cake from your own backend
A custom cake — an edible print on top, baked by a vetted local bakery, hand-delivered to the recipient's door. One HTTP call creates the gift, routes it to a bakery near the address, renders the design and the card, and checks out.
Overview
Every request goes to one base URL, over HTTPS, with a JSON body:
https://daymaker.com/api/v1/partner
The machine-readable contract is /api/v1/partner/openapi.json — public, no key needed, so an agent framework can generate tool definitions before it has credentials. The reference at the bottom of this page is rendered from that document.
This or the MCP server
We run a public MCP server too, and for an assistant talking to us directly it is the better surface: conversational, self-describing, nothing to sign up for. It is the wrong surface for a platform embedding cake-sending in its own product, because every gift it opens belongs to a throwaway anonymous account.
| MCP server | Partner API | |
|---|---|---|
| Credential | Per-gift token, anonymous | Durable dm_live_… key |
| Account | A fresh one every gift | Yours, plus a sub-account per end user |
| Retry safety | None needed — a human is in the loop | Idempotency-Key |
| Payment | A person pays a Stripe link | Saved card, prepaid balance, or a link |
Both call the same code underneath, so they cannot drift on what a valid design is or whether a New York address needs a suite number.
Quickstart
1. Create a key
Open your Daymaker dashboard, go to Connect AI, and create a key under Building this into your own product? The account owner or an admin can — a key spends on behalf of the whole account. It is shown once; we store a hash, so a lost key is revoked and replaced rather than recovered.
Keys belong to that account, so everything they send appears in the campaign list the account already reads.
2. Check the key works
curl https://daymaker.com/api/v1/partner/catalog \ -H "Authorization: Bearer $DAYMAKER_KEY"
/catalog returns sizes, shapes, from-prices and the design
rules. It writes nothing and costs nothing.
3. Send a cake
curl -X POST https://daymaker.com/api/v1/partner/cakes \
-H "Authorization: Bearer $DAYMAKER_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"recipient": {
"company": "Acme Robotics",
"contact_name": "Sara Chen",
"address": "500 Howard St",
"address2": "Suite 400",
"city": "San Francisco",
"state": "CA",
"zip": "94105",
"phone": "+14155550123"
},
"design": { "message_text": "CONGRATS SARA!" },
"card": { "message_text": "Congratulations on the raise.",
"message_signature": "— the Acme team" },
"cake_size": "8in"
}'
201 comes back with a gift_id, the routing
result, preview URLs for the design and the card, and a
checkout block — either a Stripe URL to hand your user, or
confirmation that the card on file was charged. Add
"draft_only": true to build everything and stop before
payment.
Authentication
Authorization: Bearer dm_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
X-Api-Key: dm_live_… also works, as does a bare key with no
Bearer — both are common first attempts, and a
401 on a key you are holding correctly is a miserable first
five minutes.
Server-side only. A key in a browser is a key anyone can spend. Revoke one from the same Connect AI page; it stops working immediately.
A missing, malformed, unknown or revoked key answers 401.
Nothing else on the API is anonymous.
End users
Every endpoint accepts an optional end_user — your own
opaque id for the person a gift belongs to. Send it in the body, or as
X-End-User on any request (the body wins if you send both).
- Omit it. The gift belongs to your platform account and your card pays. This is the shape when you resell cakes and bill your users yourself.
- Send it. The gift belongs to that user's own sub-account, with their own saved card, their own checkout links and their own history.
Sub-accounts are created on first use and namespaced to your key: two
platforms both using "user_1" get two unrelated accounts,
and one of your users can never read another's gift. The value is never
matched against an email or an existing customer, so naming
someone@else.com gets you a fresh row in your own namespace,
not that person's account.
It is a per-call field. You do not have to decide up front.
Send a cake
POST /cakes runs the whole loop: create, route the recipient
to a local bakery, render the design and the card, and check out. It
requires an Idempotency-Key,
because it creates and charges in one request.
It stops rather than guessing
If a step fails, the response carries completed: false and
stopped_at, and nothing is charged. The
gift_id still comes back, so the draft is recoverable with
the step-by-step endpoints.
stopped_at | What happened |
|---|---|
recipient | Undeliverable address, or a New York address with no suite line and no phone |
design | The artwork was rejected — bad SVG, unfetchable URL, too large |
card | The card you asked for could not be rendered |
checkout | Not payable — no size set, spend cap reached |
A card that was asked for and not produced stops the call. Shipping a cardless cake with a warning nobody reads is worse than failing loudly.
No address? Send a website
"recipient": {
"company": "Acme Robotics",
"contact_name": "Sara Chen",
"website_url": "https://acme.com"
}We find and verify the company's mailing address. Only a high-confidence match proceeds — anything less stops the call and hands back the candidates for you to choose from, because a cake delivered to the wrong building is not something anyone can undo.
contact_name is required and is not the company name. A
delivery addressed only to a company reaches a reception desk with nobody
to hand it to; if there is no specific person, name the receiving
department. A phone number is optional and is the difference between a
delivered cake and a returned one — the bakery calls it when nobody
answers the door.
Your own ids
Every gift-creating endpoint takes a metadata object. We
store it, hand it back verbatim on reads and in webhooks, and never look
inside it, so you do not have to keep a gift-id-to-your-object-id table.
Limits: 30 keys, 4000 characters of JSON, flat values.
{ "metadata": { "crm_deal_id": "deal_4821", "campaign": "q3-launch" } }Designs and inscriptions
design (the cake top) and card (the 4×6 card in
the box) each take exactly one artwork field:
message_text— we typeset it. A few words on the cake; a real sentence on the card.image_svg— an SVG you author, rasterized print-ready. No scripts, no external URLs.image_url— a public https PNG, JPEG or WebP, up to 10MB.image_base64— the same bytes inline.
Sending two is a 400 rather than a precedence rule: silently
printing one of them puts the wrong thing on a cake that cannot be
recalled. Responses include print_fit_warnings — resubmit
until they are clean.
A cake does not have to carry a print at all. Send
inscription and the bakery writes those words on it in
icing, by hand, up to 60 characters. That is the right shape for anything
recurring: nobody wants to commission artwork on three hundred employee
birthdays a year.
| You send | The cake gets |
|---|---|
inscription only | A plain iced cake with the words piped on it. No print. |
| One artwork field | A printed design, no writing. |
| Both | The print, and the words piped alongside it. |
| Neither | A 400. A cake has to carry something. |
On a gift going to several people the gift-wide inscription is a default and each recipient can override it, which is how one order carries ten different names.
Idempotency
POST /cakes requires an Idempotency-Key header.
Every other write accepts one, and you should send it. Any unique string
works; a UUID is ideal, and a deterministic key you can reconstruct
(birthday-emp_8891-2026-09-10) is better still.
| Situation | Response |
|---|---|
| Same key, same body | The original response, with Idempotent-Replay: true |
| Same key, different body | 422 — that is a bug on your side, and hiding it behind an unrelated response would be worse |
| Same key, first call still running | 409. Retry in a moment |
Keys are scoped to your API key and kept for 24 hours.
Money and checkout
Drafts are free. Nothing is charged and nothing is baked until checkout. At checkout we charge the card on file if there is one, and otherwise return a Stripe URL for a person to pay in a browser. You never handle card details.
By default there is always a human in the loop: an agent looping out of control produces unpaid drafts and unpaid links, and nothing bakes.
That default is wrong for a genuinely recurring workflow — a link per
birthday has automated nothing. A key can instead be set to settle from a
prepaid balance, which comes back paid with no URL and no human.
Ask us to turn it on; it carries a required 24-hour spend ceiling, and
checkout answers 429 over the ceiling with the draft saved.
curl https://daymaker.com/api/v1/partner/balance \ -H "Authorization: Bearer $DAYMAKER_KEY"
settles_from_balance is the field people misread first: a
healthy balance on a key that does not settle from it still returns a
payment link on every order. It is a property of the key, not the
account. Running dry is silent — checkout quietly goes back to returning
links — so set low_balance_usd_cents when you
register a webhook and hear about it on the way
down.
Webhooks
Rather than polling GET /gifts/{id}, register an endpoint
and we will tell you. Subscriptions are partner-scoped: one feed covers
every end user beneath your key, and a sub-account cannot register its
own endpoint, because that would be a way to receive a sibling's
deliveries.
curl -X POST https://daymaker.com/api/v1/partner/webhooks \
-H "Authorization: Bearer $DAYMAKER_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://yours.app/hooks/daymaker",
"events": ["gift.paid", "gift.delivered"]}'
The response carries the signing secret once. Omit
events to receive all of them, which also means new event
types reach you without a change on your side. The url must be https —
the payload carries recipient names and addresses.
Events
| Event | Fires when |
|---|---|
gift.paid | Checkout completed. The order is committed and the bakery is briefed |
gift.delivered | It landed. Carries the cake photo and the handover photo |
gift.delivery_failed | It could not be handed over. Carries failure_reason |
balance.low | The prepaid balance fell below your threshold. Once per dry spell |
thing.*, order.*, checkout.* | Gift-campaign fulfilment, for partners using the CRM endpoints |
Your metadata is echoed in every payload, so you can route an event without a lookup.
Verifying
X-Daymaker-Event gift.delivered X-Daymaker-Signature t=1754300000,v1=<64 hex chars> X-Daymaker-Timestamp 1754300000
v1 is HMAC-SHA256 of `${t}.${rawBody}` under
your secret. Compare in constant time and reject a timestamp more than
five minutes old. It is the same scheme Stripe uses, so an existing
helper will work.
// Node, on the raw body — not the parsed one
const [t, v1] = header.split(",").map(p => p.split("=")[1]);
const expected = crypto.createHmac("sha256", secret)
.update(`${t}.${rawBody}`).digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
&& Math.abs(Date.now() / 1000 - Number(t)) < 300;Delivery
At least once. A retry repeats the same event_key — dedupe
on it, or one cake landing becomes two rows in your database. Failures
back off over 8 attempts; an endpoint that fails 10 attempts
consecutively is switched off and says so, with
disabled_reason, in GET /webhooks. Return
2xx promptly: we time out at 10 seconds.
Errors
{
"error": {
"code": "validation_error",
"message": "Some fields are missing or invalid.",
"request_id": "req_…",
"fields": [{ "field": "recipient.contact_name", "problem": "Required" }]
}
}| Status | Meaning |
|---|---|
400 | Validation. fields says which |
401 | Missing, malformed, unknown or revoked key |
403 | A sub-account tried to act for another user |
404 | No such gift on this account — check end_user |
409 | Gift already paid, recipient cap reached, or an idempotent call in flight |
422 | Idempotency key reused with a different body |
429 | The key's 24-hour spend cap would be exceeded |
5xx | Ours. The write did not complete — retry with the same key |
request_id is in every response and every response header.
Quote it and we can find the exact call.
Guide: a cake on every birthday
The recurring-occasion integration end to end. You hold the roster; we never see a birthday, only an order for a date.
Ask about coverage before you ask the employer. There is no point offering a cake to someone whose employee lives where we cannot reach, and finding out after they said yes is the one outcome that makes the feature feel broken.
# 1 — two weeks out, is that address deliverable on that day?
curl -X POST https://daymaker.com/api/v1/partner/coverage \
-H "Authorization: Bearer $DAYMAKER_KEY" \
-H "Content-Type: application/json" \
-d '{"address": "1100 Sanchez St", "city": "San Francisco",
"state": "CA", "zip": "94114", "delivery_date": "2026-09-10"}'
covered: true comes back with prices_by_size —
the real local price, which is what you show the employer. Anything else
means don't ask: no_coverage (no bakery reaches them),
lead_time_unavailable or capacity_full (a
bakery covers them but not that day —
earliest_delivery_date says when). It is free, and no order
exists yet.
2 — Ask the employer. Your UI, your product. Nothing is created here.
# 3 — they said yes
curl -X POST https://daymaker.com/api/v1/partner/cakes \
-H "Authorization: Bearer $DAYMAKER_KEY" \
-H "Idempotency-Key: birthday-emp_8891-2026-09-10" \
-H "Content-Type: application/json" \
-d '{
"recipient": {
"company": "Roam", "contact_name": "Sarah Chen",
"address": "1100 Sanchez St", "city": "San Francisco",
"state": "CA", "zip": "94114", "phone": "+14155550123",
"delivery_date": "2026-09-10"
},
"design": { "inscription": "Happy Birthday Sarah" },
"card": { "message_text": "Happy birthday from all of us.",
"message_signature": "— the Roam team" },
"cake_size": "8in",
"metadata": { "employee_id": "emp_8891", "occasion": "birthday" }
}'No flavour to pick — the bakery bakes what it is good at — and no print to design. Make the idempotency key deterministic, as above: a retry, a redeploy mid-run, or a scheduler that fires twice then all collect the same cake instead of sending two.
4 — Photos and status. Every bakery photographs the cake and the
handover, and gift.delivered brings you both with your
metadata attached.
Two things differ when you deliver to homes rather than offices, and both bite. A phone number matters much more, because nobody mans a front desk at a house. And coverage is a real filter rather than a formality — employees live where they live, so run step 1 across the whole upcoming window and you will know months ahead who you can offer this to.
All endpoints
Rendered from
the OpenAPI document this
deployment is serving, so it is the contract itself rather than a copy of
it. Every endpoint takes the Authorization header;
every write takes Idempotency-Key.
Questions, a raised spend cap, a prepaid balance, or an integration you want us to look at: contact@daymaker.com. Prefer to let an assistant do it conversationally? That is the MCP server.