API & developers

Sent-message evidence, as an API.

Create and send messages from your own software, attach secure files, tag everything with your own IDs, and pull back a verifiable ledger — the same records your users see in the web app.

Base URL https://sentledger.com/v1 · OpenAPI 3.1 spec

curl https://sentledger.com/v1/messages \
  -H "Authorization: Bearer $SENTLEDGER_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-A-1001-estimate" \
  -d '{"to":["recipient@example.com"],"subject":"Your estimate","html":"<p>Hi {{name}}, your estimate is ready.</p>","variables":{"name":"Alex"},"metadata":{"order_id":"A-1001"},"send":true}'

Quickstart

1

Create a key

In the app, go to API & Webhooks → New key. Choose scopes like messages:write and events:read. The secret is shown once.

2

Send a message

POST /v1/messages with "send": true. Add an Idempotency-Key header so retries never double-send.

3

Read the ledger

GET /v1/messages/:id/evidence returns JSON, or add ?format=pdf|csv|zip for exports.

Authentication & scopes

Send Authorization: Bearer sl_live_…. Each key belongs to one workspace and carries only the scopes you grant. Keys can expire, be restricted to IP addresses, and be revoked instantly.

messages:write · messages:read · events:read · contacts:read · contacts:write · files:write · files:read · templates:read · templates:write · webhooks:manage · billing:read

Conventions

  • Errors: { error: { code, message, details, request_id } }
  • Cursor pagination with limit, cursor, from, to
  • Rate limits reported in X-RateLimit-* headers
  • Idempotency: same key + same body replays; different body → 409

Webhooks

Subscribe to message.sent, delivered, opened, clicked, file_viewed, bounced, failed, revoked and billing.usage_threshold. Every delivery is signed; failures retry 8 times over about 45 hours, and you can replay any delivery from the dashboard.

import crypto from "node:crypto";

// Express/Hono/etc: use the RAW request body.
export function verifySentLedger(rawBody: string, header: string, secret: string) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // 5-minute tolerance
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ""));
}

Browser extensions & integrations

Extensions (Chrome, Thunderbird) and partner integrations use a device-authorization flow: the client requests a code from POST /v1/device/code, the user approves it at /app/device, and the client receives a scoped key from POST /v1/device/token. Every client reports into the same message and event model.

Keep a record you can stand behind.

Start with the web app today and bring in the API when you're ready. No card required for the trial.

Start free trialTalk to sales