Mr. Handled

API & integrations

A handwritten card, the moment it matters.

When a deal closes, a client signs or a donation comes in, your tools can send a card on their own: written individually for that person from your template, then handwritten and mailed. New integrations start in approval mode, so you see every card before it’s paid for.

Quick start

  1. Set up your business profile (name, signer, return address) in the dashboard.
  2. Create an API key under Integrations. Pick its permissions and whether its cards wait for approval (the default) or send on their own.
  3. POST a send. The card is written within a minute and appears in “Approve these”.
curl
curl -X POST https://sohandled.com/api/v1/sends \
  -H "Authorization: Bearer bt_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: deal-4821-thank-you" \
  -d '{"recipient":{"name":"Maria Lopez","address_line1":"14 Elm Street","address_line2":"Apt 2","city":"Austin","state":"TX","postal_code":"78701","country":"US"},"template":"client-thank-you","merge_fields":{"property_address":"14 Elm Street","closing_date":"2026-06-30"},"arrive_by":"2026-11-20","format":"card","external_id":"deal-4821","schedule_anniversary":true}'

Authenticate with Authorization: Bearer bt_live_… (or X-Api-Key). Keys are stored hashed, shown once, scoped (sends:write, sends:read), and show when they were last used. Revoke one any time.

Create a send

POST https://sohandled.com/api/v1/sends. Send the same Idempotency-Key header (or idempotency_key) when you retry: you get the same send back, never a second card. Without one, external_id (or the exact body) does the same job.

Request body
{
  "recipient": {
    "name": "Maria Lopez",
    "address_line1": "14 Elm Street",
    "address_line2": "Apt 2",
    "city": "Austin",
    "state": "TX",
    "postal_code": "78701",
    "country": "US"
  },
  "template": "client-thank-you",
  "merge_fields": {
    "property_address": "14 Elm Street",
    "closing_date": "2026-06-30"
  },
  "arrive_by": "2026-11-20",
  "format": "card",
  "external_id": "deal-4821",
  "schedule_anniversary": true
}
recipient
name (first and last), address_line1, address_line2, city, state, postal_code, country (default US). Or first_name + last_name.
template
A saved template’s id (number), or a built-in: client-thank-you, referral-thank-you, home-anniversary, client-birthday, holiday-client, thanksgiving-gratitude, checking-in, staff-thank-you, donor-first-gift, donor-repeat-gift, donor-major-gift, guest-thank-you, guest-welcome, guest-stay-anniversary, guest-repeat-thank-you, customer-note, customer-holiday-new-year, customer-holiday-christmas, customer-holiday-happy-holidays, customer-holiday-thanksgiving, customer-holiday-valentines, customer-holiday-fourth-of-july. Leave it out to use your first saved template.
merge_fields
Values for the template’s {fields}, up to 30.
arrive_by
Optional YYYY-MM-DD. We work back to the mailing date (an estimate); too soon becomes “as soon as possible”.
format
"card" (folded, default), "flat" or "letter" (a full page).
external_id
Your record id, returned with the status and usable to look sends up.
assign_to
Optional: the email of someone on your team. The card is theirs: signed with their name, listed in their Cards, and theirs to approve when it waits. Left out, the card is the Owner’s (or, for a client in your client book, that client’s person’s). An email that isn’t on your team is refused (422). Webhooks: map a field to “Assign to”; no match sends it as the Owner’s.
schedule_anniversary
true + closing_date: also schedules the 1-year home anniversary card, arriving by that date’s anniversary.
gift
Nonprofits: amount, date, designation, donor_type ("first" or "repeat"), transaction_id. Picks the gift template.

Merge fields: {first_name}, {last_name}, {full_name}, {property_address}, {city}, {home_years}, {company}, {signer}, {closing_date}, {amount}, {designation}, {gift_date}. Each card is still written individually by our writer from your template, in its tone; merge fields are facts it uses, not a mail merge.

Response (201 Created)
{
  "id": 1042,
  "status": "received",
  "hold_reason": null,
  "template": "client-thank-you",
  "template_name": "Client thank-you",
  "format": "folded-portrait",
  "external_id": "deal-4821",
  "arrive_by": "2026-11-20",
  "scheduled_for": null,
  "price_cents": null,
  "order_id": null,
  "mailed_at": null,
  "estimated_arrival": null,
  "error": null,
  "created_at": "2026-10-10T18:04:11.000Z",
  "replayed": false,
  "anniversary": {
    "id": 1043,
    "status": "scheduled",
    "arrive_by": "2027-06-30",
    "scheduled_for": "2027-06-09"
  }
}

Also: GET /api/v1/sends?external_id=… (recent sends, newest first) and GET /api/v1/templates (your saved templates and the built-ins).

Status

GET https://sohandled.com/api/v1/sends/<id> with a key that has sends:read.

scheduled
A home anniversary card waiting for its day (writing starts 3 weeks before it should arrive).
received
Stored; the writer starts within a minute.
writing
The card is being written for this recipient.
awaiting_approval
Waiting in “Approve these” (approval mode, or over this month’s spend cap).
awaiting_payment
Approved, waiting for payment (no saved-card billing yet, no card on file, or a declined card).
approved
Approved and being charged (monthly billing charges once a day).
paid
Paid; queued for the pen (or waiting for its mailing date).
being_written
Being handwritten by Mr. Handled.
mailed
Stamped and mailed; mailed_at and estimated_arrival are set.
rejected
You removed it from the queue.
canceled
Canceled and refunded before mailing.
failed
Couldn’t be written (see error) or couldn’t be sent.

Prices, billing and the spend cap

1–9 per charge

$5.95

a card · $7.95 a letter, stamp included

10–24 per charge

$5.45

a card · $7.45 a letter, stamp included

25+ per charge

$5.10

a card · $7.30 a letter, stamp included

  • Per request (default): one card on its own is the single price; approving many at once (“Approve all”) is one charge at that group’s tier.
  • Monthly billing (opt in): your cards this calendar month count toward the 10–24 and 25+ tiers. Auto-sent cards are charged together once a day. Each charge carries its own card-processing fee, so a charge of only a card or two may be priced at a smaller tier.
  • Spend cap: $500 a month by default, editable. Your dashboard shows this month’s spend against it, and cards beyond the cap wait for your approval.
  • Paying: with saved-card billing on, approved cards are charged to your saved card. Until then (or without a card), they wait in “Approve these” and you pay for any number of them with one tap on a secure Stripe page.

Zapier: Webhooks by Zapier

  1. 1

    Make an API key

    In your dashboard, Integrations → API keys → New key. Copy it: it’s shown once.

  2. 2

    Create a Zap

    Pick the trigger (a deal closing, a new client, a date). Add a Filter step if only some should get a card.

  3. 3

    Add “Webhooks by Zapier” → POST

    URL: the sends endpoint below. Payload type: json. Add the Headers Authorization (Bearer + your key) and Idempotency-Key.

  4. 4

    Map the fields

    In Data, add recipient__name, recipient__address_line1, recipient__city, recipient__state, recipient__postal_code, template and merge_fields__… (Zapier turns __ into nested JSON).

  5. 5

    Test, then turn it on

    The test card lands in “Approve these” (approval mode is on for new keys), so nothing is charged until you approve it.

URL https://sohandled.com/api/v1/sends

Payload type json

Headers Authorization: Bearer bt_live_… and Idempotency-Key: {{Deal ID}}

Prefer no key in Zapier? Use your webhook URL from the dashboard instead and map the fields there (below).

Deal closed → client thank-you (and the home anniversary next year)

Trigger: Your CRM’s “Deal stage changed” or “Deal won” trigger (filter: stage is Closed).

Data (as JSON)
{
  "recipient": {
    "name": "{{Contact Name}}",
    "address_line1": "{{Mailing Street}}",
    "city": "{{Mailing City}}",
    "state": "{{Mailing State}}",
    "postal_code": "{{Mailing Zip}}"
  },
  "template": "client-thank-you",
  "merge_fields": {
    "property_address": "{{Property Address}}",
    "closing_date": "{{Close Date}}"
  },
  "external_id": "deal-{{Deal ID}}",
  "schedule_anniversary": true
}

New client → welcome card

Trigger: “New contact” in your CRM, or “New row” in a Google Sheet of clients.

Data (as JSON)
{
  "recipient": {
    "name": "{{Full Name}}",
    "address_line1": "{{Street}}",
    "city": "{{City}}",
    "state": "{{State}}",
    "postal_code": "{{Zip}}"
  },
  "template": "client-thank-you",
  "merge_fields": {
    "company": "{{Your Company}}"
  },
  "external_id": "client-{{Contact ID}}"
}

Client anniversary → card that arrives on the day

Trigger: “Schedule by Zapier” (every day) + a lookup of clients whose anniversary is 3 weeks away, or your CRM’s date-based trigger.

Data (as JSON)
{
  "recipient": {
    "name": "{{Client Name}}",
    "address_line1": "{{Street}}",
    "city": "{{City}}",
    "state": "{{State}}",
    "postal_code": "{{Zip}}"
  },
  "template": "home-anniversary",
  "merge_fields": {
    "home_years": "{{Years}} years",
    "property_address": "{{Property Address}}"
  },
  "arrive_by": "{{Anniversary Date}}",
  "external_id": "anniversary-{{Client ID}}-{{Year}}"
}

Your inbound webhook URL

Every business can make webhook URLs (Integrations → New webhook). The secret is in the URL. Send JSON, a form post, or a GET with query parameters from any tool. The first request is saved as a sample: pick which of its fields is the name, street, city, state and ZIP (and any merge fields), optionally only send when a field equals a value (stage = Closed), and choose the template.

Optional HMAC: turn on “Require a signature” and send X-BT-Signature: sha256=<hex>, the HMAC-SHA256 of the raw body with the webhook’s secret.

Signing a request
// Node.js: sign the exact body you send.
import { createHmac } from 'node:crypto'
const body = JSON.stringify(payload)
const signature = createHmac('sha256', process.env.BT_WEBHOOK_SECRET).update(body).digest('hex')
await fetch(webhookUrl, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'X-BT-Signature': `sha256=${signature}` },
  body,
})

Real estate CRMs

Follow Up Boss

  1. In Follow Up Boss: Admin → API → copy your API key (the account owner’s key).
  2. In your dashboard: Integrations → Follow Up Boss → paste it.
  3. Pick which stages count as closed (for example “Closed”) and the template each sends. Deal pipeline stages work too.
  4. Leave “Home anniversary” on to schedule the 1-year card, arriving by the closing date’s anniversary (a “Closing date” custom field if you have one, else the deal’s close date or the day it closed).

We listen for Follow Up Boss’s stage-change webhooks, or check for stage changes every hour. People who were already closed when you connect don’t get a card. The card goes to the person’s home or mailing address in Follow Up Boss.

BoldTrail (kvCORE)

BoldTrail sends webhooks from Smart Campaigns as a GET with query parameters. Make a webhook URL in your dashboard, then in BoldTrail: Templates → add a Webhook, paste the URL, and add query parameters (name, street, city, state, zip) using BoldTrail’s variables. Add the webhook as an action in the Smart Campaign you run when a deal closes. Your first request becomes the sample to map.

Query parameters
?name={lead_name}&street={seller_address}&city={seller_city}&state={seller_state}&zip={seller_zip}

For deal or transaction triggers BoldTrail doesn’t send itself, use the Zapier route above.

Any other CRM (Lofty, Sierra, Salesforce, HubSpot…): use its webhook with your webhook URL, or Zapier.

Donations

Nonprofits can send a thank-you for every new donation from their donation form or Zapier: include gift and we pick the first-gift, repeat-gift or major-gift template. Each gift is remembered, so the same donation is never thanked twice, and a later CSV upload of it is skipped.

A donation
{
  "recipient": {
    "name": "Dana Whitfield",
    "address_line1": "220 Oak Avenue",
    "city": "Columbus",
    "state": "OH",
    "postal_code": "43215"
  },
  "gift": {
    "amount": "150.00",
    "date": "2026-10-03",
    "designation": "Summer reading program",
    "donor_type": "first",
    "transaction_id": "ch_3Q8x"
  },
  "idempotency_key": "donation-ch_3Q8x"
}

Errors and limits

201 / 200
Created / the same send again (a retry with the same idempotency key; "replayed": true).
401 / 403
Missing or revoked key / the key lacks the scope.
409
The idempotency key was used for a different body, or the business profile isn’t set up.
422
A field needs fixing: "errors" names each one, like "recipient.postal_code".
429
More than 300 sends or 900 reads per key per 15 minutes.

Every key, webhook, approval and charge is in your dashboard’s activity log.

Cards that send themselves.

Start in approval mode: nothing is charged until you approve a card.