Private beta is open. Public beta opens December 1.Join the private beta →

Join private beta →Book a demo

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).

EventWhen it's sent
scan.recordedSomeone scanned a contact's QR code.
address.resolvedResearch settled: mailable, held, not found or failed.
lead_list.address_resolvedThe same, for a person in a lead list.
enrollment.updatedAn 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.

Do the same from Clay →
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

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.

Build mail into your stack.

Create a key in Settings → API keys and send your first contacts.

Let's see if Mailyard fits.

Three quick questions, then pick a time for a 30-minute demo.

How many people do you want to reach each month?
Do you already have a lead list?