Code examples
Replace env vars. Partner signature = HMAC-SHA256 of {timestamp}\n{rawBody}.
1. Node.js — signed partner send
const crypto = require('crypto');
const API_KEY = process.env.BLASTS_API_KEY;
const SECRET = process.env.BLASTS_SIGNING_SECRET;
const BASE = 'https://api.blasts.app/api/v1/partner';
async function partnerRequest(method, path, bodyObj) {
const body = bodyObj == null ? '' : JSON.stringify(bodyObj);
const ts = String(Date.now());
const sig = crypto.createHmac('sha256', SECRET).update(`${ts}\n${body}`).digest('hex');
const res = await fetch(`${BASE}${path}`, {
method,
headers: {
...(body ? { 'Content-Type': 'application/json' } : {}),
Authorization: `Bearer ${API_KEY}`,
'X-Blasts-Timestamp': ts,
'X-Blasts-Signature': `sha256=${sig}`,
...(bodyObj?.metadata?.tracking_number
? { 'Idempotency-Key': String(bodyObj.metadata.tracking_number) }
: {})
},
body: body || undefined
});
return res.json();
}
// Package delivered (single)
await partnerRequest('POST', '/messages/send', {
to_email: 'customer@example.com',
body_plain: 'Your package 1Z999 was delivered at 2:14 PM.',
body_html: '<p><strong>Delivered</strong></p>',
priority: 'urgent',
request_read_ack: true,
metadata: { tracking_number: '1Z999' }
});
1b. Node.js — batch send (org key only, max 500)
// Employee seat tokens CANNOT call /messages/batch (403 org_api_key_required).
const batch = await partnerRequest('POST', '/messages/batch', {
batch_name: 'fedex_sunday_digest',
body_plain: 'Hi {{name}}, update on {{tracking}}.',
body_html: '<p>Hi {{name}}, update on {{tracking}}.</p>',
priority: 'normal',
recipients: [
{ to_email: 'a@example.com', merge: { name: 'Ann', tracking: '1ZAAA' } },
{ to_email: 'b@example.com', merge: { name: 'Bob', tracking: '1ZBBB' } }
]
});
// batch.batch_id · batch.queued · batch.skipped
// Sends complete async; each success emits message.delivered webhook.
1c. Node.js — named campaign + poll status
const created = await partnerRequest('POST', '/campaigns', {
campaign_name: 'spring_digest',
body_plain: 'Hi {{name}} — your update.',
recipients: [
{ to_email: 'a@example.com', merge: { name: 'Ann' } },
{ to_email: 'b@example.com', merge: { name: 'Bob' } }
]
});
// created.campaign_id · created.batch_id · created.queued
async function waitForCampaign(campaignId) {
for (let i = 0; i < 40; i++) {
const st = await partnerRequest('GET', `/campaigns/${campaignId}`);
if (st.status === 'completed' || st.status === 'failed') return st;
await new Promise((r) => setTimeout(r, 1500));
}
throw new Error('campaign poll timeout');
}
// await waitForCampaign(created.campaign_id);
2. Node.js — mint an employee seat (no secrets in the app binary)
// Org backend mints once; native app stores only employee_token.
const seat = await partnerRequest('POST', '/employees', {
label: 'Dave — iOS console',
daily_send_cap: 200,
external_ref: 'okta:dave'
});
// seat.employee_token → "emp_live_…" (shown once)
// Native app send — Bearer only, NO HMAC:
async function employeeSend(employeeToken, payload) {
const res = await fetch(`${BASE}/messages/send`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${employeeToken}`
},
body: JSON.stringify(payload)
});
return res.json();
}
3. Node.js — configure + verify webhooks
// Configure (org key + HMAC)
const cfg = await partnerRequest('POST', '/webhook', {
url: 'https://your.app/hooks/blasts',
events: ['message.read', 'message.dismissed', 'message.reaction', 'enrollment.confirmed']
});
// cfg.webhook_secret → store in your vault (shown once)
// Express receiver
app.post('/hooks/blasts', express.raw({ type: 'application/json' }), (req, res) => {
const ts = req.get('X-Blasts-Webhook-Timestamp') || '';
const sig = (req.get('X-Blasts-Webhook-Signature') || '').replace(/^sha256=/, '');
const raw = req.body.toString('utf8');
const expected = crypto.createHmac('sha256', process.env.BLASTS_WEBHOOK_SECRET)
.update(`${ts}.${raw}`).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(sig, 'hex'), Buffer.from(expected, 'hex'))) {
return res.status(401).end();
}
if (Math.abs(Date.now() - Number(ts)) > 5 * 60 * 1000) return res.status(401).end();
const evt = JSON.parse(raw);
// dedupe on evt.id / X-Blasts-Event-Id
console.log(evt.type, evt.data);
res.status(200).json({ ok: true });
});
// After POST { test: true }, inspect recent deliveries (org key + HMAC)
const log = await partnerRequest('GET', '/webhook/deliveries?limit=20');
// log.deliveries[0] → { event_id, type, http_status, ok, attempt, at, last_error }
// Never contains webhook_secret
4. Python — partner send
import hashlib, hmac, json, os, time, requests
API_KEY = os.environ["BLASTS_API_KEY"]
SECRET = os.environ["BLASTS_SIGNING_SECRET"].encode()
BASE = "https://api.blasts.app/api/v1/partner"
def partner_post(path, payload):
body = json.dumps(payload, separators=(",", ":"))
ts = str(int(time.time() * 1000))
sig = hmac.new(SECRET, f"{ts}\n{body}".encode(), hashlib.sha256).hexdigest()
r = requests.post(
BASE + path,
data=body,
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {API_KEY}",
"X-Blasts-Timestamp": ts,
"X-Blasts-Signature": f"sha256={sig}",
},
timeout=30,
)
r.raise_for_status()
return r.json()
partner_post("/messages/send", {
"to_email": "customer@example.com",
"body_plain": "Your appointment is confirmed for Tuesday at 10 AM.",
"priority": "normal",
})
5. Client API — outbox + long-poll (native apps)
const ORIGIN = 'https://api.blasts.app';
async function clientPost(path, body) {
const res = await fetch(`${ORIGIN}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
return res.json();
}
// Sent-message sync
const outbox = await clientPost('/api/ping/outbox', {
from_email, machine_id, session_token, limit: 25
});
// Near-realtime loop (foreground)
async function watchInbox({ recipient_email, machine_id, session_token }) {
let since_rev = 0;
for (;;) {
const wait = await clientPost('/api/ping/events/wait', {
recipient_email, machine_id, session_token, since_rev, wait_ms: 20000
});
if (wait.changed) {
since_rev = wait.rev;
const inbox = await clientPost('/api/ping/inbox', {
recipient_email, machine_id, session_token
});
render(inbox.messages);
}
}
}
6. Client API — upload attachment then send
const up = await clientPost('/api/ping/attach/upload', {
from_email, machine_id, session_token,
mime_type: 'image/jpeg',
filename: 'pod.jpg',
media_b64: fs.readFileSync('pod.jpg').toString('base64')
});
await clientPost('/api/ping/send', {
from_email, to_email, code: 'K7M2-9P3Q',
body: 'Proof of delivery attached.',
machine_id, session_token,
attachments: [{
url: up.attachment_url,
mime: up.mime,
filename: up.filename,
size: up.size
}]
});
7. Bash / cURL — signed Partner calls (runnable)
Requires openssl + curl. Signature payload is ${timestamp}\n${rawBody} (empty body for GET).
#!/usr/bin/env bash
set -euo pipefail
BASE="https://api.blasts.app/api/v1/partner"
API_KEY="${BLASTS_API_KEY:?set BLASTS_API_KEY}"
SECRET="${BLASTS_SIGNING_SECRET:?set BLASTS_SIGNING_SECRET}"
partner_curl() {
local method="$1" path="$2" body="${3-}"
local ts sig
ts="$(node -e 'process.stdout.write(String(Date.now()))')"
sig="$(printf '%s\n%s' "$ts" "$body" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $NF}')"
if [ -n "$body" ]; then
curl -sS -X "$method" "${BASE}${path}" \
-H "Authorization: Bearer ${API_KEY}" \
-H "X-Blasts-Timestamp: ${ts}" \
-H "X-Blasts-Signature: sha256=${sig}" \
-H "Content-Type: application/json" \
--data-binary "$body"
else
curl -sS -X "$method" "${BASE}${path}" \
-H "Authorization: Bearer ${API_KEY}" \
-H "X-Blasts-Timestamp: ${ts}" \
-H "X-Blasts-Signature: sha256=${sig}"
fi
echo
}
# Verify credentials
partner_curl GET /me
# Single send
partner_curl POST /messages/send '{"to_email":"customer@example.com","body_plain":"Hello from cURL"}'
# Batch (org key only; max 500)
partner_curl POST /messages/batch '{"batch_name":"curl_demo","body_plain":"Hi {{name}}","recipients":[{"to_email":"a@example.com","merge":{"name":"Ann"}}]}'
# Webhook test ping (after POST /webhook config)
partner_curl POST /webhook '{"test":true}'
# Recent webhook delivery attempts
partner_curl GET '/webhook/deliveries?limit=20'
# Outbox pull
partner_curl GET '/messages?limit=25'
← Full API reference · Rate limits · OpenAPI YAML · Interactive Redoc · API Terms