Send API

Live

Email infrastructure. Verify a domain, mint a key, post a message. The same engine carries receipts, sign-in codes, newsletters and campaigns, on separate sending pools so a marketing blast can never dent a password reset. Console at bolrach.com/send. Device push is Push.

Base URL and keys

Every public path sits under https://api.bolrach.com/send/v1. Keys are Bolrach Keys: create one at bolrach.com/account/keys with the Send module enabled and the permissions it needs (transactional, marketing, manage, read). A key can be locked to your sending domains. Keys start with bk_live_; older snd_live_ keys keep working. Send the key as a bearer token.

curl "https://api.bolrach.com/send/v1/status" \
  -H "Authorization: Bearer bk_live_your_key_here"

Streams

Every message declares what kind of mail it is. The stream picks the sending pool and the rules.

streamUse it forRules
securitySign-in codes, password resets, new-device alertsHighest-trust pool. No tracking, never promotional. 600 per minute per workspace.
transactionalReceipts, invoices, order and account noticesNo promotional content. 300 per minute.
subscribedNewsletters and campaignsOnly to contacts with recorded consent. Own IP and HELO. 60 per minute by default; raised as the pool warms.
repermissionOne-time reconfirmation of an old listIsolated pool, 10 per minute.

Send a message

POST /send/v1/messages

Returns 202 Accepted with a message_id. Accepted is not delivered; watch the status or a webhook for that. to takes one address or a list. Pass an Idempotency-Key and a retry of the same request is answered from the first attempt instead of sending twice.

curl -X POST "https://api.bolrach.com/send/v1/messages" \
  -H "Authorization: Bearer bk_live_your_key_here" \
  -H "Idempotency-Key: order_4821_receipt" \
  -H "Content-Type: application/json" \
  -d '{"stream":"transactional","from":{"email":"receipts@yourdomain.com","name":"Your Shop"},"to":"customer@example.com","subject":"Receipt for order 4821","html":"<p>Thanks. Your order is on its way.</p>","text":"Thanks. Your order is on its way.","reply_to":"help@yourdomain.com","tags":["receipt"],"metadata":{"order_id":"4821"}}'

Optional: template_id with variables renders a console template instead of subject and html; headers adds custom headers. The from domain must be verified in the workspace, or use the sandbox sender below.

GET /send/v1/messages/{id}

Current status and the event timeline for one message: sent, delivered, deferred, bounced, complained, suppressed or failed.

curl "https://api.bolrach.com/send/v1/messages/MESSAGE_ID" \
  -H "Authorization: Bearer bk_live_your_key_here"

Domains

Add the domain you send from, publish the records the response lists (SPF include, two DKIM CNAMEs and a DMARC policy), then call verify. Sending from an unverified domain is refused.

POST /send/v1/domains
curl -X POST "https://api.bolrach.com/send/v1/domains" \
  -H "Authorization: Bearer bk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"domain":"yourdomain.com"}'
POST /send/v1/domains/{domain}/verify

Checks the records and flips the domain to active. GET /send/v1/domains lists them; GET /send/v1/domains/{domain} shows one with its record status.

curl -X POST "https://api.bolrach.com/send/v1/domains/yourdomain.com/verify" \
  -H "Authorization: Bearer bk_live_your_key_here" \
  -H "Content-Type: application/json"

Sandbox. Before a domain is verified, send from sandbox@bolrach.email. It works only for addresses inside your own workspace, so nothing reaches a stranger by mistake.

Contacts and consent

The subscribed stream and every campaign go only to contacts whose consent is on record. A contact added through the API starts as pending and receives a confirmation link; importing a list with a consent source and date records it as confirmed. Consent events are kept for each contact.

POST /send/v1/contacts
curl -X POST "https://api.bolrach.com/send/v1/contacts" \
  -H "Authorization: Bearer bk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"email":"reader@example.com","source":"footer-form","attributes":{"plan":"pro"}}'
POST /send/v1/contacts/import
curl -X POST "https://api.bolrach.com/send/v1/contacts/import" \
  -H "Authorization: Bearer bk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"contacts":[{"email":"reader@example.com","consent_source":"checkout","consent_at":"2026-08-01T10:00:00Z","brand_relationship":"customer"}]}'
GET /send/v1/contacts?status=confirmed

Filter by pending, confirmed or unsubscribed. GET /send/v1/contacts/{email} shows one contact with its consent history; POST /send/v1/contacts/{email}/unsubscribe unsubscribes one by hand; POST /send/v1/contacts/reconfirm starts a repermission run for an old list.

Campaigns and newsletters

A campaign is a subject and body sent to the confirmed audience of the workspace. Every campaign message carries List-Unsubscribe and List-Unsubscribe-Post headers (RFC 8058) and an unsubscribe link in the footer, both signed per contact, so Gmail and Yahoo can honour a one-click unsubscribe and the contact drops out before the next send. Use {{unsubscribe_url}} in your own template to place the link yourself.

POST /send/v1/campaigns
curl -X POST "https://api.bolrach.com/send/v1/campaigns" \
  -H "Authorization: Bearer bk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"name":"September letter","subject":"What changed this month","from_email":"news@yourdomain.com","html":"<h1>What changed</h1><p>Three things worth knowing.</p>","audience":"confirmed"}'
POST /send/v1/campaigns/{id}/send

Queues one message per confirmed contact on the campaign pool and answers with the recipient count. GET /send/v1/campaigns lists campaigns and GET /send/v1/campaigns/{id} shows one with its counts.

curl -X POST "https://api.bolrach.com/send/v1/campaigns/CAMPAIGN_ID/send" \
  -H "Authorization: Bearer bk_live_your_key_here" \
  -H "Content-Type: application/json"

The gate. Marketing sending pauses itself when the complaint rate crosses 0.3 percent or bounces climb. Transactional and security mail keep flowing. The console shows why and lets you resume once the list is cleaned.

Webhooks

Register an endpoint in the console under Webhooks and pick the events. Each delivery is a JSON POST with id, type, created and data, signed with the endpoint secret. Failed deliveries retry with backoff; the console lists every attempt with the response code.

EventWhen
message.sentThe receiving server accepted the message
message.deliveredDelivery confirmed
message.deferredThe receiver asked us to try later
message.bouncedRejected for good; the address is suppressed
message.complainedThe reader marked it as spam; the contact is unsubscribed
message.suppressedNot sent because the address is on the suppression list
message.failedCould not be sent after retries
campaign.sentA campaign finished queueing

Verify the send-signature header (t=timestamp,v1=hex): HMAC SHA-256 of timestamp.rawBody with the endpoint secret. Reject anything older than five minutes. x-send-event names the event and x-send-delivery is the delivery id, which stays the same across retries so you can dedupe.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifySendWebhook(rawBody: string, signature: string, secret: string) {
  const parts = Object.fromEntries(signature.split(",").map((p) => p.split("=")));
  const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return fresh && timingSafeEqual(Buffer.from(parts.v1, "hex"), Buffer.from(expected, "hex"));
}

Also on the API

  • POST /send/v1/verify and /send/v1/verify/bulk: check whether addresses are deliverable before you send.
  • GET /send/v1/templates: templates managed in the console, referenced by template_id.
  • GET /send/v1/analytics/overview: sent, delivered, bounced and complained over a window.
  • GET /send/v1/usage and GET /send/v1/status: quota, sending state and whether the marketing gate is closed.

Errors come back as 400 with a plain message (missing fields, an unverified from domain, a key scoped to another stream, a rate limit, the gate), 401 for a missing or revoked key.