oh, dearly!

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

  1. 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 with odk_live_.
  2. Set its policy there: the kinds it may send, the most per send and per month, and whether it may send without asking.
  3. Call the API with the key as a bearer token: https://api-production-375f.up.railway.app

API

GET/v1/agent/policyThe 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=30Moments coming up (birthdays, anniversaries, the dates that matter), with the last gift each person had.
GET/v1/agent/piecesThe 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/quoteWhat 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/lettersSend 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}/cancelWithdraw a send that waits for approval. Once the studio has a letter, only the owner can stop it.
POST/v1/agent/momentsSomething 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-requestsA 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.quotedA price and an arrival day were given
send.approvedA gesture was approved and ordered
send.writtenIt was written or made, and photographed
send.photographedIts photo passed our check
send.postedIt was posted
send.deliveredIt was delivered
send.cancelledIt was stopped before making
address.confirmedSomeone gave their address
address.expiredAn address request ran out without an answer
send.reactionSomeone reacted or said thank you
invitation.openedA guest opened their invitation page
invitation.rsvpA guest answered an invitation
approval.pendingA send waits for someone's approval, with the reason
agent.limit_reachedAn 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.