v1 · stable · GA since 21 Oct 2025

Email infrastructure with a REST you'll like.

Send transactional and marketing email from your code. EU-only by default. Idempotent, signed webhooks, BLUN AI inference on tap. Node, Python, PHP, Ruby SDKs.

base url: https://api.blun.ai/v1
auth: Bearer token
region: eu-de
ratelimit: 10 r/s burst 60
curl · example
# send your first transactional email
curl https://api.blun.ai/v1/transactional/send \
  -H "Authorization: Bearer $PULSE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from":    "hello@blun.ai",
    "to":      "anna@workshop.de",
    "subject": "Your samples are on the way",
    "html":    "<p>Hi Anna, …</p>",
    "tags":    ["order_confirmation"]
  }'
{ "id": "msg_84f3a2e1c8b5", "status": "queued", "queue_position": 1, "region": "eu-de", "estimated_delivery_ms": 240 }

Overview

The BLUN-Mailing API is a REST API over JSON. Send to the EU edge endpoint at api.blun.ai/v1 — your data lands in EU-hosted Nuremberg or Falkenstein, never leaves the EU. Authentication is Bearer token, idempotency via Idempotency-Key header, errors return RFC 9457 problem-details JSON.

Base URL
api.blun.ai/v1
Region default
eu-de
Auth
Bearer token
Errors
RFC 9457
EU-only by default. All inference, storage, and processing routes through EU-hosted Nuremberg + Falkenstein. Every response carries X-Region: eu-de so your audit trail can prove residency. To explicitly select FAL only, send X-Region: eu-de-fal.

Authentication

Create an API key at /admin/settings/api-keys. Keys are scoped to a workspace and can be limited to read-only or per-resource (e.g. transactional-send only). Live keys are prefixed pulse_live_; test keys are pulse_test_.

# export PULSE_KEY=pulse_live_…
curl https://api.blun.ai/v1/me \
  -H "Authorization: Bearer $PULSE_KEY"
# → {"workspace":"BLUN HQ","scope":"full","region":"eu-de"}
Keep keys server-side. Never embed a live key in a browser, mobile app, or public repo. Use a short-lived token from your backend if you need a client to call the API directly.

Errors & idempotency

All errors follow RFC 9457 problem-details JSON: type, title, status, detail, instance. Idempotency is opt-in by sending an Idempotency-Key header on POST — the same key replayed within 24 h returns the cached first response.

curl -X POST https://api.blun.ai/v1/transactional/send \
  -H "Authorization: Bearer $PULSE_KEY" \
  -H "Idempotency-Key: order_84f3_confirm_2026-05-04" \
  -H "Content-Type: application/json" \
  -d '{ … }'

# Replaying same Idempotency-Key returns the cached msg_id.

HTTP status codes

StatusMeaningYou should
200OKContinue.
201CreatedResource created. Use returned id.
202AcceptedQueued for async send. Listen for webhook.
400Validation errorInspect detail; fix request.
401AuthCheck key + scope.
409ConflictIdempotency-Key collision with different body.
422UnprocessableDKIM/SPF/DMARC misalignment, suppressed recipient, etc.
429Rate limitedHonor Retry-After; back off.
5xxServerRetry with same Idempotency-Key.

Rate limits

Per-workspace rate limit is 10 requests/second sustained, burst 60. Per-domain send limit follows your warmup curve. X-RateLimit-Remaining is included on every response.

Sustained
10 r/s
Burst
60 r
Webhook delivery
100 r/s
Send queue depth
unlimited

Regions & residency

Default region is eu-de (EU-hosted Nuremberg primary, Falkenstein replica). Workspace owners on Pro+ can pin to eu-de-nue or eu-de-fal only. AI inference always rotates through both DE-NUE and DE-FAL — never outside the EU. See /security#data.

POST /v1/transactional/send

Send a single email. Returns immediately with a msg_id; the actual SMTP handoff happens asynchronously and webhooks are delivered for delivered, bounced, opened, clicked.

Body parameters

FieldTypeReq?Description
fromstringrequiredSender address. Domain must be verified in /admin/domains.
tostring | arrayrequiredSingle address or array of up to 50 BCC-style recipients (no cross-leak).
subjectstringrequired≤ 255 chars. Liquid templating supported when contact_id is set.
htmlstringrequired*Or text. At least one body field must be set.
textstringoptionalPlain-text alternative. Auto-derived from html if absent.
tagsstring[]optionalUp to 20 tags for analytics filtering.
contact_idstringoptionalLinks the send to a contact for unified analytics + Liquid merge fields.
attachmentsarrayoptionalUp to 10 files, ≤ 25 MB total. Each: {filename, content_b64, mime}.
scheduled_atRFC 3339 datetimeoptionalDefer send. Up to 60 days ahead.
curl -X POST https://api.blun.ai/v1/transactional/send \
  -H "Authorization: Bearer $PULSE_KEY" \
  -H "Idempotency-Key: order_84f3_confirm" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "hello@blun.ai",
    "to":   "anna@workshop.de",
    "subject": "Your leather samples are on the way",
    "html":    "<p>Hi Anna, your 3 swatches shipped today.</p>",
    "contact_id": "con_91Hk2",
    "tags":       ["order_confirmation", "shipped"]
  }'

POST /v1/audience/contacts

Create or upsert a contact. Match-key is email per-workspace. Returns 201 on create, 200 on update. Triggers contact.created and any tag-added automations.

FieldTypeReq?Description
emailstringrequiredRFC 5322 valid; lowercased + suppression-list checked before insert.
first_namestringoptional≤ 80 chars.
tagsstring[]optionalExisting or new — auto-created if not present.
consentobjectrequired (EU){source, ip, ts, double_opt_in_at} — written to consent ledger.
const resp = await pulse.audience.contacts.upsert({
  email: "giulia@conceria-castello.it",
  first_name: "Giulia",
  tags: ["leather", "italy", "verified"],
  consent: {
    source:   "checkout_form",
    ip:       "5.75.143.92",
    ts:       "2026-05-04T14:18:00Z",
    double_opt_in_at: "2026-05-04T14:19:42Z"
  }
});

POST /v1/campaigns

Create a campaign in draft. Use /campaigns/:id/send to ship, or set scheduled_at to defer. The same shape powers A/B variants — pass an array under variants with split percentages.

A/B variants

{
  "name": "Product launch update",
  "audience": { "tag": "newsletter" },
  "variants": [
    { "key": "A", "split_pct": 50, "subject": "Product launch update" },
    { "key": "B", "split_pct": 50, "subject": "How Anna sourced from Bergamo, Tirupur and Wenzhou — one chat" }
  ],
  "winner_metric": "open_rate",
  "winner_call_at_p": 0.05
}

POST /v1/automations/:id/trigger

Fire a custom event into an automation. The flow's trigger node decides whether the contact enters the flow. Idempotent on (automation_id, contact_id, event_key).

curl -X POST https://api.blun.ai/v1/automations/auto_welcome_v3/trigger \
  -H "Authorization: Bearer $PULSE_KEY" \
  -d '{
    "contact_id": "con_91Hk2",
    "event_key":  "checkout.completed",
    "data": {
      "order_id": "ord_84f3",
      "value_eur": 340
    }
  }'

POST /v1/ai/subject-suggestions

Get 5 subject-line suggestions from the BLUN AI gateway. Inference is EU-only (Frankfurt + Falkenstein); inputs are dropped within 30 seconds; the model is never trained on your content.

FieldTypeReq?Description
body_htmlstringrequiredCampaign HTML — used only for inference, not stored.
toneenumoptionaldirect · warm · punchy · formal. Default: matches your last 30 days winning patterns.
languageISO-639-1optionalAuto-detected from body_html if absent.
avoid_emojibooleanoptionalDefault false.
BLUN AI pledge. Every response carries X-AI-Region: eu-de. Inputs + outputs are dropped within 30 s. We don't sign training-rights agreements with any model provider. Per-workspace opt-out at /admin/settings/ai. Data sheet →

POST /v1/domains/:id/verify

Trigger DKIM / SPF / DMARC re-check for a domain. Idempotent. Returns current status + the exact DNS records you should publish.

POST /v1/suppressions

Add an address to your workspace suppression list. Hashed before storage; BLUN-Mailing will reject any future send to it (returns 422 with type=suppressed).

GET /v1/messages

List sent messages with cursor-based pagination. Filter by status (queued/sent/delivered/bounced/opened/clicked), tag, contact_id, or date_range.

Webhooks · signing v2

Each webhook is signed with HMAC-SHA256 over timestamp + "." + body. Headers: X-Pulse-Signature: v2,t=<ts>,sig=<hex>. Replay window 5 minutes. Old keys honored 30 days during rotation.

import { createHmac, timingSafeEqual } from "crypto";

function verify(req, secret) {
  const [_, t, sig] = req.headers["x-pulse-signature"].match(/v2,t=(\d+),sig=([a-f0-9]+)/);
  if (Date.now()/1000 - +t > 300) throw new Error("replay");
  const expect = createHmac("sha256", secret).update(`${t}.${req.rawBody}`).digest("hex");
  if (!timingSafeEqual(Buffer.from(sig), Buffer.from(expect))) throw new Error("sig");
}

Webhooks · event catalog

message.queuedqueued for SMTP handoff
message.delivered250 ok received from receiver MTA
message.bouncedhard or soft bounce; payload includes diagnostic-code
message.complainedFBL report from AOL/Outlook/Yahoo/T-Online/GMX/web.de/La Poste
message.openedtracking pixel fetched (privacy-respecting; configurable per workspace)
message.clickedtracking link followed; payload includes target_url + redirect chain
contact.creatednew contact upserted
contact.tag.addedtag applied; triggers automation entry checks
contact.unsubscribedopt-out via footer link or DSAR portal
campaign.send.completedall recipients dispatched
automation.enteredcontact entered an automation
automation.completedcontact reached an end node
domain.verifiedDKIM+SPF+DMARC alignment achieved
team.invitation.acceptedworkspace seat used

Webhooks · retries & ordering

If your endpoint returns non-2xx, BLUN-Mailing retries with exponential backoff: 5 s, 30 s, 2 min, 10 min, 30 min, 2 h, 6 h, 24 h (8 attempts over 24 h). After that the event lands in a dead-letter queue at /admin/webhooks/dlq. Ordering is best-effort per-resource; consume idempotently using the X-Event-Id header.

SDKs

First-party SDKs for the four most-asked-for runtimes. All wrap the same v1 surface and ship the same retry/idempotency defaults.

Node @blun-ai/BLUN-Mailing · 4.2.1 npm i @blun-ai/BLUN-Mailing
Python BLUN-Mailing · 2.6.0 pip install BLUN-Mailing
PHP blun-ai/BLUN-Mailing · 1.9.0 composer require blun-ai/BLUN-Mailing
Ruby BLUN-Mailing · 1.7.0 gem install BLUN-Mailing

Going via raw HTTP from another runtime? The OpenAPI 3.1 spec is at /api/openapi.json. Import into Insomnia, Bruno, or your generator of choice.