Developers
Direct mail as an API call.
A REST API for address research, lead lists, enrollments, QR codes and scans, with signed webhooks for results.
POST /api/v1/address-research { "campaign": "q4-reopen", "contacts": [ { "contactId": "hs-48213", "email": "dana@ledgerline.example", "company": "Ledgerline", "city": "Columbus", "region": "OH" } ] } → 202 · accepted 1 · status "queued"
Authentication
One key per integration. Server-side only.
Send an organization API key as a bearer token. Owners create keys under Settings → API keys.
- Base URL
https://app.mailyard.ai - JSON in and out, one error shape naming each rejected field
Authorization: Bearer myk_your_api_key
Content-Type: application/json{
"error": {
"code": "validation_failed",
"message": "…",
"details": { "contacts[0].identity": "…" }
}
}Resources
What you can do with it.
Campaigns use a label you choose (or their id), lead lists an id.
Address research
USPS-verified business addresses, with evidence.
- POST
/api/v1/address-researchQueue up to 500 contacts - GET
/api/v1/campaigns/{label}/addressesStatus, address, verification, confidence, sources
Lead lists
Research a list once and mail it from any campaign later.
- POST
/api/v1/lead-listsCreate a list - GET
/api/v1/lead-listsLists, newest first, paged - POST
/api/v1/lead-lists/{id}/contactsAdd up to 500 people - GET
/api/v1/lead-lists/{id}/contactsEach person's result, paged
Always-on campaigns and enrollments
Enroll leads one at a time into an approved, funded campaign.
- GET
/api/v1/campaigns?mode=standingYour always-on campaigns - GET
/api/v1/campaigns/{label}One campaign, with the fields its letter needs - POST
/api/v1/campaigns/{label}/enrollmentsEnroll one contact - POST
/api/v1/campaigns/{label}/enrollments/batchEnroll up to 500 - GET
/api/v1/campaigns/{label}/enrollmentsPaged list, filter by status - GET
/api/v1/campaigns/{label}/enrollments/{contactId}One enrollment - DELETE
/api/v1/campaigns/{label}/enrollments/{contactId}Cancel before print
QR codes and scan links
A tracked link and styled QR code per contact.
- POST
/api/v1/scan-linksMint or return one contact's link and code - GET
/api/v1/qr-style-optionsShapes, eyes, frames, logos, colors - POST
/api/v1/qr-previewsRender a style without storing it - PUT
/api/v1/campaigns/{label}/qr-styleSet a campaign's QR style
Scans
A campaign's links and scans in one read.
- GET
/api/v1/campaigns/{label}/scansRecipients, scan counts and events
Letter pages and letter links
Added pages as print-ready HTML, and private letter pages for follow-up email.
- PUT
/api/v1/campaigns/{label}/letter-pagesUp to five added pages - GET
/api/v1/campaigns/{label}/lettersHosted letter link per mailed person
Idempotency
Safe to retry. Safe to re-run.
Your contactId is the idempotency key. Post the same person twice and you get their current state back.
Enrollments
201 when created, 200 if already enrolled.
Scan links
Retries return the same token and code.
Always 202 for research
Read the status or wait for the webhook.
Webhooks
Results arrive as they happen.
Signed events POST to your https endpoint, retrying after 1m, 5m, 30m, 2h, 12h and 24h. Pick nested or flat (for Clay).
| Event | When it's sent |
|---|---|
| scan.recorded | Someone scanned a contact's QR code. |
| address.resolved | Research settled: mailable, held, not found or failed. |
| lead_list.address_resolved | The same, for a person in a lead list. |
| enrollment.updated | An always-on enrollment changed status or delivery stage. |
Answer 2xx within 5 seconds and dedupe on the Mailyard-Delivery id.
POST https://hooks.example.com/mailyard Mailyard-Event: scan.recorded Mailyard-Delivery: 7c0e… Mailyard-Signature: t=1791900000,v1=<hex> { "type": "scan.recorded", "data": { "contactId": "hs-48213", "scannedAt": "2026-10-16T15:02:11.000Z", "phase": "live" } }
Verifying signatures
Check each delivery before you trust it.
v1 is the hex HMAC-SHA256 of "<t>.<raw body>" under your endpoint's secret. Reject timestamps over five minutes off.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(secret, header, rawBody) {
const parts = Object.fromEntries(
header.split(",").map((p) => p.trim().split("="))
);
const t = Number(parts.t);
const now = Math.floor(Date.now() / 1000);
if (!Number.isInteger(t) || Math.abs(now - t) > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
return /^[a-f0-9]{64}$/i.test(parts.v1 ?? "") &&
timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(parts.v1, "hex"));
}Example
Enroll a lead into an always-on campaign.
The campaign must be active, approved and funded. A DELETE cancels before print.
POST /api/v1/campaigns/q4-reopen/enrollments { "contactId": "hs-48213", "firstName": "Dana", "lastName": "Reyes", "email": "dana@ledgerline.example", "company": "Ledgerline", "website": "ledgerline.example", "city": "Columbus", "region": "OH" } → 201 · status "queued" # later, by enrollment.updated researching → verifying → scheduled → printing → in_transit
Questions
Is there a public API reference?
Not yet. Email us and we'll send it, with help on your first integration.
Can I call the API from a browser?
No. A key acts for your whole organization, so keep it server-side.
Is there a rate limit?
10 requests a second per key, with a burst of 20. Over that you get 429 rate_limited with a Retry-After header.
How do I test an integration safely?
Send a webhook.test event from the endpoint's test action. A test enrollment is scheduled 24+ hours out and can be cancelled before it prints.
Related
Add direct mail as a step in a Clay table. Each row enrolls into an always-on campaign that finds, verifies and mails the lead.
All integrationsBring lists from Clay, HubSpot, Salesforce, Apollo or any CSV. Enroll leads through the API and get scans back by webhook.
Mail analyticsTrack each letter from printing to delivery, give each contact a QR code, and see who scanned, when and on what device.
SecurityHow Mailyard keeps customer data separate, keeps personal data out of logs, signs webhooks and makes sure opted-out people aren't mailed.
Build mail into your stack.
Create a key in Settings → API keys and send your first contacts.