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}'const res = await fetch("https://sentledger.com/v1/messages", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SENTLEDGER_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "order-A-1001-estimate",
},
body: JSON.stringify({
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,
}),
});
const message = await res.json();
// Later: fetch the evidence ledger
const ledger = await fetch(`https://sentledger.com/v1/messages/${message.id}/evidence`, {
headers: { Authorization: `Bearer ${process.env.SENTLEDGER_API_KEY}` },
}).then((r) => r.json());import os, requests
r = requests.post(
"https://sentledger.com/v1/messages",
headers={
"Authorization": f"Bearer {os.environ['SENTLEDGER_API_KEY']}",
"Idempotency-Key": "order-A-1001-estimate",
},
json={
"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,
},
)
message = r.json()
ledger = requests.get(
f"https://sentledger.com/v1/messages/{message['id']}/evidence",
headers={"Authorization": f"Bearer {os.environ['SENTLEDGER_API_KEY']}"},
).json()<?php
$ch = curl_init("https://sentledger.com/v1/messages");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("SENTLEDGER_API_KEY"),
"Content-Type: application/json",
"Idempotency-Key: order-A-1001-estimate",
],
CURLOPT_POSTFIELDS => json_encode([
"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,
]),
]);
$message = json_decode(curl_exec($ch), true);Quickstart
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.
Send a message
POST /v1/messages with "send": true. Add an Idempotency-Key header so retries never double-send.
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.