For developers
oh, dearly! sends letters that are written by hand by a person and photographed before they are posted. Your software and your AI assistant use the same primitives the app uses: people, moments, letters, policies, address requests, sends, and events.
Start
- In oh, dearly!, open Account, then Agents, and create a key. A test key starts with
odk_test_: nothing is made or posted, and tracking runs on its own in five minutes. A live key starts withodk_live_. - Set its policy there: the kinds it may send, the most per send and per month, and whether it may send without asking.
- Call the API with the key as a bearer token:
https://api-production-375f.up.railway.app
API
| GET | /v1/agent/policy | The rules this key works within: the kinds it may send (letters, and the kinds of pieces that may go with one), the most per send and per month, what is left, whether it may send alone. |
| GET | /v1/agent/relationships?q= | People: id, name, company, role, city, country, notes, their next moment, the last gift. Never contact details. |
| GET | /v1/agent/moments?days=30 | Moments coming up (birthdays, anniversaries, the dates that matter), with the last gift each person had. |
| GET | /v1/agent/pieces | The small shelf of pieces that may go with a letter: price, compliance tier, whether this key may send that kind. A piece never goes alone. |
| POST | /v1/agent/letters/quote | What a handwritten letter to one person would cost and when it would arrive: the card (more words make it a longer letter), upgrades, pieces, postage at cost. Nothing is sent. |
| POST | /v1/agent/letters | Send a handwritten letter in the owner's voice, with pieces if any. The key's policy decides: it goes, it waits for approval, or it is refused with a reason. |
| GET | /v1/agent/sends/{id} | Where a send is: waiting, declined, made, photo checked, posted, delivered. |
| POST | /v1/agent/sends/{id}/cancel | Withdraw a send that waits for approval. Once the studio has a letter, only the owner can stop it. |
| POST | /v1/agent/moments | Something happened: a deal closed, a new role, a meeting. It goes on the person's timeline; the workspace's triggers decide whether a letter follows. |
| POST | /v1/agent/address-requests | A private link the person uses to give their own address, open for the days you set (two weeks unless set). The address never comes back. |
A price, then a letter
curl https://api-production-375f.up.railway.app/v1/agent/letters/quote \
-H "Authorization: Bearer odk_test_..." \
-H "Content-Type: application/json" \
-d '{"recipientId":"6f1c...","upgrades":{"seal":true},"arriveBy":"2026-12-18",
"text":"Congratulations on closing the round. Lunch in January?"}'curl https://api-production-375f.up.railway.app/v1/agent/letters \
-H "Authorization: Bearer odk_test_..." \
-H "Content-Type: application/json" \
-d '{"recipientId":"6f1c...","upgrades":{"seal":true},"arriveBy":"2026-12-18",
"text":"Congratulations on closing the round. Lunch in January?",
"idempotencyKey":"deal-4411-closing"}'A send answers 201 when it went out, 202 when it waits for a person to approve it in the app, and 422 with a reason when the policy refuses it. The same idempotencyKey twice never makes two letters. Prices are net, in minor units (cents) with their currency. A piece goes only with a letter, through pieceIds.
What an agent cannot do alone
- It never sees an address, an email or a phone number. People give their own address through an address request.
- A person who said no to gifts is never sent anything. No person hears from agents more than twice in 30 days.
- Every send stays within the key's policy: the kinds it may send, the most per send, the monthly budget. Above it, a person approves.
- Every call is in the agent's log, in the app.
Events
Add a webhook under Account, Agents. Each message is a JSON body with id, type, createdAt and data (ids and states, never contact details), signed in the ohdearly-signature header. A failed delivery is retried after 1, 5, 30, 120, 360 and 1440 minutes.
Webhook delivery starts on 3 November 2026. Until then you can add an endpoint, and events wait for it.
send.quoted | A price and an arrival day were given |
send.approved | A gesture was approved and ordered |
send.written | It was written or made, and photographed |
send.photographed | Its photo passed our check |
send.posted | It was posted |
send.delivered | It was delivered |
send.cancelled | It was stopped before making |
address.confirmed | Someone gave their address |
address.expired | An address request ran out without an answer |
send.reaction | Someone reacted or said thank you |
invitation.opened | A guest opened their invitation page |
invitation.rsvp | A guest answered an invitation |
approval.pending | A send waits for someone's approval, with the reason |
agent.limit_reached | An agent key reached its monthly limit |
Checking a signature
import { createHmac, timingSafeEqual } from "node:crypto";
// header "ohdearly-signature": t=<unix seconds>,v1=<hex HMAC-SHA256 of "t.body">
export function verify(secret, body, header) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = createHmac("sha256", secret).update(`${parts.t}.${body}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300;
return fresh && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ""));
}MCP server
AI assistants connect to oh, dearly! over the Model Context Protocol with the same key and the same rules. Tools: what I may do, find people, find moments, record what happened, list the pieces, price a letter, send a letter, track, cancel, ask for an address.
Hosted, nothing to install
The server is at https://api-production-375f.up.railway.app/mcp (Streamable HTTP), with the agent key as a bearer token. In Claude Code:
claude mcp add --transport http oh-dearly https://api-production-375f.up.railway.app/mcp \ --header "Authorization: Bearer odk_test_..."
Sign in instead of a key
Assistants that connect by OAuth, such as Claude's custom connectors, need only the address https://api-production-375f.up.railway.app/mcp. You sign in to oh, dearly!, choose test or live and set the rules, and the assistant gets a key of its own, listed and revocable under Agents. Discovery is at https://api-production-375f.up.railway.app/.well-known/oauth-authorization-server; PKCE (S256) is required.
Coming
HubSpot: letters started from deal stages and dates, logged on the contact's timeline, with the reaction written back.