[INIT]

Get started

Create a tenant to get your first API key, or load an existing tenant you already have a key for.

How this works, in plain English

What's a "tenant"?

This platform is multi-tenant — one shared system, but many separate customers using it at once, each with their own domains, campaigns, and unsubscribe lists that are completely invisible to everyone else. A "tenant" is just the technical name for one of those customers. If you're a business signing up, you (or your whole company) are one tenant.

What's an API key for?

It's how the system knows a request is really coming from you. Think of it like a password, except it's long and random instead of something you'd type from memory, and it's meant to be pasted into scripts/tools rather than into a login form. Every request you make (except creating a tenant in the first place) needs to include your key in an X-API-Key header, or it gets rejected.

Why is it shown only once?

The server never stores your key in a form it could show back to you — it only stores a one-way scrambled version (a "hash"). That way, even if someone broke into the database, they couldn't read out usable keys. The tradeoff: if you lose your key, there's no "forgot password" recovery. You'd need to generate a new one (which itself requires already having a working key — see the API keys section).

Why is creating a tenant the only step that needs no key?

Every other action needs a key, but you can't have a key before your very first tenant exists — so this one step is deliberately open to anyone, and it hands you your tenantId and first key together, atomically, in the same response.

Create a new tenant

This is a one-time bootstrap step. It mints your first API key in the same response — there's no other way to get one, and no way to look it up again afterward.

Load an existing tenant

Already have a tenant ID and API key? Load them here to use the rest of this console.

[DOM]

Sending domains

Register a domain with DKIM before sending from it. First registration on the platform wins — a domain someone else already verified can't be re-registered by a different tenant.

How this works, in plain English

Why do I need to prove I own a domain before sending from it?

Without proof, anyone could send email pretending to be "you@yourcompany.com" — that's literally what most email spoofing/phishing is. Mailbox providers (Gmail, Outlook, etc.) look for cryptographic proof that a message really came from the domain it claims to, and refuse or spam-flag anything that can't prove it.

What is DKIM, actually?

DKIM (DomainKeys Identified Mail) is that proof. When you register a domain here, OCI Email Delivery generates a private signing key it keeps to itself, and a matching public key it publishes as a DNS record. Every email you send gets invisibly signed with the private key; the receiving mail server looks up the public key in DNS and checks the signature matches. If it does, the receiver knows the message genuinely passed through Email Delivery on your behalf and hasn't been tampered with in transit.

What's a CNAME record?

A CNAME record is just a DNS entry that says "this name is an alias for that name" — like a forwarding address, but for domain names instead of mail. Email Delivery generates one DKIM key and hands back its DNS name/value directly, so there's just one CNAME record to add here (some providers, including this platform's own AWS original, use 3 for signing-key rotation — Email Delivery's model is simpler).

Where do I add this record?

At your domain registrar or DNS host (GoDaddy, Namecheap, Hostinger, Cloudflare, Route 53 — wherever you manage this domain's DNS). Look for a "DNS management" or "DNS records" page, and add it exactly as shown, using the "CNAME" record type.

Why doesn't "Verify" work immediately after I add the record?

DNS changes take time to spread across the internet's many DNS servers, but in practice that's not the long pole here: confirmed by directly checking DNS the whole time during this platform's own testing, the CNAME was live and correct within minutes — DKIM verification itself still took about 50-55 minutes end to end, purely because that's how often Email Delivery's own verification job runs, not a DNS propagation delay. A "not verified yet" result right after adding the record is completely normal — wait a while and check again. (SPF, below, verifies faster in practice — around 20-30 minutes.)

What are the SPF and DMARC records it also recommends?

These are two more DNS-based, email-authentication standards, layered on top of DKIM as extra trust signals. SPF's absence doesn't block sending in practice, but DKIM's does — Email Delivery's send call hard-requires DKIM to be verified, confirmed by a real rejection when attempting to send before it was. SPF is a list of servers allowed to send mail for your domain. DMARC tells receivers what to do if a message fails both SPF and DKIM (starting at "just watch and report," never "reject," so it can never accidentally block your own legitimate mail). Both meaningfully improve inbox placement over time.

Register a domain

Registers with Email Delivery (DKIM) and returns the CNAME record (plus recommended SPF/DMARC records) to add at your DNS provider.

Your domains

Loaded live from the server (GET /tenants/{tenantId}/domains) -- this is every domain your tenant has registered, on any device.

[CAM]

Campaigns

Send to a JSON list of addresses, or upload a CSV file — both go through the identical pipeline.

How this works, in plain English

What actually happens when you click "Send campaign"?

Several OCI services hand off to each other in sequence, each doing one specific job:

1. Object Storage — your subject/body/recipients get written as two files (content.json + recipients.csv). Writing the recipients file is what triggers everything below, the same mechanism whether you paste addresses or upload a CSV.
2. A Function (ingest_campaign) notices the new file, checks each address against your suppression list, and enqueues one message per recipient onto a queue — in chunks of 2,000 at a time for large campaigns, self-invoking to pick up where it left off.
3. OCI Queue — a buffer between "figure out who to email" and "actually send." It exists so a large campaign doesn't have to happen instantly and all at once; it decouples how fast recipients can be queued from how fast Email Delivery will let you actually send.
4. Connector Hub picks messages off that queue and invokes another Function (send_email_worker) per recipient, which builds the actual email (with your unsubscribe link, physical address footer, etc.) and hands it to Email Delivery to send.
5. Email Delivery delivers it. Unlike some email platforms, there's no real-time bounce/complaint push event here — a separate scheduled Function (bounce_poller) polls Email Delivery's suppression list every so often and reflects new bounces/complaints back into this platform's own database, which is what GET /campaigns/{id} and this console's "Campaign status" card actually read from.

The reason for this many moving pieces: at real scale (thousands+ recipients), you can't just loop through a list and send one-by-one in a single request — it would time out long before finishing. Splitting the work into small, independent, retryable steps is what lets this same pipeline handle a handful of recipients or many thousands without changing anything. (One known, real limitation at the largest scale: if a single ingestion chunk itself runs long enough to hit the ingest Function's own timeout, the campaign can get stuck mid-ingestion with no automatic recovery yet — cancel and retry if a very large campaign's status looks frozen.)

Why so many recipient checks (typo, disposable, role-based, unreachable)?

Each one catches a different real-world mistake, and each is either a hard rule or just a heads-up, never both confused for the other:

Invalid format (no @, no domain) — dropped outright, since it can never be delivered.
Unreachable domain (no mail server at all, e.g. a typo like gmial.com with no DNS mail records) — dropped, since it would just bounce.
Possible typo (a domain one letter off from gmail.com, yahoo.com, etc.) — still sent, just flagged, because it might genuinely be someone's real, differently-spelled domain.
Disposable address (Mailinator, 10-minute-mail, etc.) — still sent, just flagged, since the mailbox is usually real right now, just likely to be abandoned soon.
Role-based address (support@, admin@) — still sent, just flagged, since it's perfectly deliverable and sometimes exactly who you mean to reach — just statistically more likely to complain or ignore marketing mail than a named person.

Why do I have to enter a physical address and a "consent basis"?

Both are legal requirements for commercial email, not this platform's own preference. CAN-SPAM (the US law governing commercial email) requires every marketing email to include a real physical postal address — this platform adds it as a small footer automatically so you never forget. The consent basis (e.g. "existing customer relationship") is stored permanently as your own record of why you believed you were allowed to email this list, in case a complaint or legal inquiry ever asks — it's never shown to recipients, purely an audit trail for you.

What's an idempotency key?

A safety net against double-sends. If your internet connection drops right as you click "Send" and you're not sure it went through, resubmitting with the same idempotency key returns the original campaign instead of sending everything a second time. Leave it blank for a normal, one-off send.

What's the webhook / Reply-To for?

Both are optional. A webhook URL gets a one-time automated notification (via a plain HTTPS request) once your campaign finishes sending to everyone, so you don't have to keep checking back manually. Reply-To lets you send from one address (like no-reply@) while replies land in a different inbox a real person actually reads (like support@) — useful since marketing sends often shouldn't come from an inbox anyone's watching.

New campaign

Only supported when sending via pasted addresses or the preview below — not with CSV upload.

Campaign status

Loaded live from the server (GET /tenants/{tenantId}/campaigns) -- your 25 most recent campaigns, on any device.

[SUP]

Suppressions

Addresses this tenant will never email again — from a hard bounce, a complaint, 3 soft bounces in a row, or an unsubscribe click.

How this works, in plain English

Why does an address end up here?

Four ways, all permanent: a hard bounce (the mailbox doesn't exist), a complaint (the recipient hit "mark as spam"), 3 soft bounces in a row (e.g. a mailbox that's been full for a while, treated as effectively dead), or the recipient clicking the unsubscribe link in one of your emails. Every future campaign checks this list first and silently skips anyone on it — there's no way to accidentally email someone who's been suppressed.

Can I remove someone from this list?

Not through this console, and not through the API either — suppression is permanent by design. It's the mechanism that keeps this platform's (and your) sending reputation healthy with mailbox providers like Gmail, so it's deliberately not something a UI button can undo.

Why do some addresses I know are suppressed not show up here?

This list only shows addresses suppressed on or after this feature's release (2026-07-07) — anything suppressed earlier doesn't carry the internal field this lookup needs, so it can't be found. It's still fully suppressed and still being enforced on every send; it just won't appear in this particular list.

Suppressed addresses

Loaded live from the server (GET /tenants/{tenantId}/suppressions), most recently suppressed first.

[KEY]

API keys

Issue an additional key for rotation, or a separately-named key for e.g. a CI pipeline. Not how you get your first key — that comes from creating a tenant.

How this works, in plain English

Why would I want more than one key?

Named, separate keys let you tell things apart later — e.g. one key for a CI/deployment pipeline, one for a teammate, one for this console — and revoke just one of them if it's ever exposed, without breaking everything else that uses a different key.

Why do I need an existing key just to make a new one?

Otherwise, anyone who ever saw your tenant ID (which isn't a secret — it's in every URL and response) could mint themselves a brand-new, fully working key for your tenant with zero proof they're actually you. Requiring a real, valid key first closes that door.

What if I lose every key I have?

There's currently no self-service "forgot my key" recovery, by design (see "Get started" for why keys can't just be looked up again). Recovery would need direct operator intervention on this platform's own infrastructure — worth keeping at least one key somewhere safe (a password manager, a secrets vault) rather than only in your browser.

Issue an additional key

No recovery if every key is lost

Only a hash of each key is ever stored server-side — a raw key is shown exactly once, here, and can't be retrieved again. If every key for a tenant is lost, there's currently no self-service recovery path.