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