API and webhooksThe same surface our dashboard runs on

REST in.
Signed events out.

Scoped keys with per-minute limits. Idempotent writes. HMAC-signed webhooks with five retries and a dead-letter view. No hidden surface, no second-class API.

Section 1 · Quickstart

Key to first event in three steps.

No OAuth dance, no app review queue. Create a key, point us at an endpoint, and the first signed event shows up the moment something happens in your workspace.

Create a scoped keyStep 1
wmbly_live_8f3kq2…600 req/min
events:readmailboxes:write

Pick scopes in the dashboard. The key can never do more than what you picked, and a cross-scope call returns 403 with the scope it was missing.

Register your endpointStep 2
POSTapi.acme.dev/hooks/mailfor
whsec_9d41kk…HMAC-SHA256

Paste a URL, get a signing secret. Subscribe to one surface or the whole stream, and rotate the secret any time without losing delivery history.

Catch the first eventStep 3
type    reply.classified
verdict positive
your reply200 OK · 41ms

Verify the signature in constant time, return 2xx inside ten seconds, process async. That is the whole contract.

Keys and endpoints live in Settings · every delivery is inspectableAfter step three it is just HTTP
Section 2 · Pick your client

One operation. Every client.

A published OpenAPI 3.1 spec you can generate a typed client from, and plain HTTP from anywhere else. Same request shape, same idempotency semantics, same error codes.

// list-campaigns.ts — plain fetch, no SDK required
const res = await fetch(
  "https://api.mailfor.tech/v1/campaigns?limit=20",
  { headers: { Authorization: `Bearer ${process.env.MAILFOR_KEY}` } },
);

const { data, pagination } = await res.json();
console.log(data.length, pagination.next_cursor);
# list_campaigns.py — plain requests, no SDK required
import os, requests

res = requests.get(
    "https://api.mailfor.tech/v1/campaigns",
    headers={"Authorization": f"Bearer {os.environ['MAILFOR_KEY']}"},
    params={"limit": 20},
)

body = res.json()
print(len(body["data"]), body["pagination"]["next_cursor"])
// main.go — net/http, no SDK required
req, _ := http.NewRequest("GET",
    "https://api.mailfor.tech/v1/campaigns?limit=20", nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("MAILFOR_KEY"))

res, err := http.DefaultClient.Do(req)
if err != nil { log.Fatal(err) }
defer res.Body.Close() // decode { data, pagination }
# terminal
$ curl https://api.mailfor.tech/v1/campaigns?limit=20 \
    -H "Authorization: Bearer $MAILFOR_KEY"

# -> { "data": [ ... ], "pagination": { "next_cursor": "c1_…", "has_more": true } }
Section 3 · Event catalog

Five surfaces, one firehose.

Subscribe to the whole stream or pick the surfaces you care about. The same event the dashboard reads is the event you receive, in the same shape.

Replies2 events

Every inbound that lands on a sequenced thread, with the classifier verdict attached.

reply.receivedreply.classified
Deliverability4 events

Hard signals from mailbox providers and recipient action. Suppressed workspace-wide on first pass.

bounce.hardbounce.softcomplaint.receivedunsubscribe.received
Mailboxes3 events

Lifecycle of each sender, from the first warmup pass through quarantine and back.

mailbox.connectedmailbox.quarantinedmailbox.recovered
Sequences3 events

Pipeline state the dashboard shows you, in the same shape the dashboard reads.

sequence.startedsequence.completedcampaign.paused
CRM2 events

Contacts, deals, pipeline stage changes mirrored to the integration on the same event.

contact.createddeal.stage_changed
Subscribe to all
events: ["*"]

Or pick by surface, by type, or by a single sequence or mailbox. The dispatcher filters before the network hop.

Section 4 · Delivery

Signed bodies. Replayable. Dead-letter visible.

Every event is HMAC-signed with your endpoint secret and carries an idempotency key. A 2xx in ten seconds closes the delivery. Anything else, and we walk a five-step retry schedule before the event lands in a view you can replay from.

  • Signed with your secret. Verify the signature in constant time before you trust the body.
  • Ten seconds to ack. Return 2xx fast and process async.
  • Dead-letter is a screen. Inspect, replay a range, rotate the secret without losing history.
POST /webhooks/yours
reply.classified
at-least-once
Event fires on the consumerStart
Payloadsigned t+ts.body
id   evt_01HQX9F7P3A8KY2NJM4R6BWT0S
type reply.classified
idem reply:th_01HQX…
2xx in under 10sacknowledged

Delivery closes. A duplicate retry from us is a no-op on your side.

non-2xx or timeoutwalk the retry schedule
01+30sMost transient outages resolve here.
02+2mReceivers that throttle briefly recover.
03+10mCovers a short rolling restart.
04+1hCovers a maintenance window.
05+6hLast attempt before dead-letter.
After 5 failed attemptsdead-letter view
Signed payload, idempotent on event idReplay any range from the dashboard
Section 5 · Per-key scopes

Scope down. Stay down.

Six resources, two verbs, one admin scope. A cross-scope call returns 403 with scope_required and the exact scope the call needs.

  • readList, inspect, subscribe.
  • writeCreate, mutate, delete.
  • adminWorkspace, billing, members.
ResourceReadWriteAdmin
Mailboxesmailboxes:readmailboxes:write
Sequencessequences:readsequences:write
Contactscontacts:readcontacts:write
Eventsevents:read
CRMcrm:readcrm:write
Workspaceadmin:write
403 scope_required carries the exact scope the call needsKeys can also be pinned to one mailbox or sequence
Section 6 · By the numbers

The whole rate policy, in one row.

600/ min
default per-key limit
per key, not per workspace
120/ min
read-only key limit
reads get their own lane
5tries
webhook retry attempts
backoff from 30s to 6h
10s
webhook delivery timeout
then it counts as a miss
24h
idempotency-key window
same key, same result
256KB
event payload ceiling
full body, never truncated
Developer FAQ

The questions we get a lot.

Full API reference and the OpenAPI spec at docs.mailfor.tech.

Compute HMAC-SHA256 of the raw request body using your endpoint secret. Compare the hex digest to the X-MailFor-Signature header in constant time. Reject any request older than 5 minutes against X-MailFor-Timestamp.

No. We deliver at least once. Every event carries an idempotency_key. Persist it on first receipt and skip duplicates. Retries happen on any non-2xx response or a timeout above 10 seconds.

The event lands in the dead-letter view in the dashboard. From there you can inspect the last failure, replay one event or a range, or rotate the endpoint secret without resetting delivery history.

Yes. Each key carries a list of scopes and an optional resource filter, for example mailboxes:write limited to mbx_01HQX. Cross-resource calls return 403 with a scope_required error code.

Yes, at docs.mailfor.tech/openapi.json. It is OpenAPI 3.1, so you can import it into Postman or Insomnia, generate a typed client in your language, or build straight against it. There are no official SDKs yet, so plain HTTP is the path today.

Wire MailFor into your stack.

Create a key, paste a webhook URL, ship the integration before lunch.