Skip to content

Email Capture

Every endpoint on an account also receives email at its own mailhooks.cc address. Preview the HTML safely, copy one-time codes and links, pick out fields of your own, read just the reply, check SPF, DKIM and DMARC, and download the original message.

Updated Nov 2026

Every endpoint that belongs to an account has an email address next to its HTTP URL. Mail sent to it lands in the same request list as your webhooks, so a signup flow that sends a webhook and a confirmation email can be checked in one place.

The address

The address is the endpoint's slug at mailhooks.cc:

my-endpoint@mailhooks.cc

Copy it from the endpoint bar above the request list, or from Settings, under Receiving.

Add a tag after a plus sign to tell test runs or flows apart. Tagged mail lands on the same endpoint, and the tag is shown on the email in the list and in the detail view:

my-endpoint+signup@mailhooks.cc
my-endpoint+password-reset@mailhooks.cc

The slug is not case sensitive. The tag is kept exactly as the sender wrote it.

mailhooks.cc is the same service with the mail first: the same account, endpoints and plans, a dashboard that opens on emails, and ready-made layouts for common senders. The switcher beside the name at the top of every page moves between the two sites, and signed in, it keeps you signed in. An endpoint is listed on the site it was made on: change that under Settings, Receiving (Listed on: webhooks.cc, mailhooks.cc or both). Either way it keeps its HTTP URL and its email address. Endpoints made before the two sites are listed on both, and so are those made through the API, the SDKs, the CLI or the MCP server unless they name a site. Emails we send you (quota, alerts) link to the site you signed in on last.

Which endpoints receive email

Endpoints on an account do, on every plan, including endpoints shared with a team. Endpoints created without an account (guest endpoints on the landing page) receive HTTP requests only, because anyone who knows a guest slug can read what it captured. Mail to a guest endpoint is refused.

Your own domain

On Business, and for a Team Business team, mail can also arrive at a domain you own, such as orders@inbound.acme.com. Add the domain under Account > Domains (a team's on the team's page, by the owner), then add two records where the domain's DNS is managed:

TypeNameValue
TXT_mailhooks.inbound.acme.commailhooks-verify= and the page's token
MXinbound.acme.commx.mailhooks.cc

Use a subdomain that receives no other mail: the MX record sends all of its mail here. Check records looks them up, and DNS changes can take a while to show. Once the domain is verified, its mail is accepted within a minute.

Then choose where its mail goes: an address per endpoint (orders@ to one endpoint, invoices@ to another), and an endpoint for any other address, or refuse those. A plus tag works as on mailhooks.cc addresses: orders+test@inbound.acme.com lands where orders@ does. Only endpoints on the account (or shared with the team) can receive a domain's mail.

The records are checked again every day. After three failed checks in a row the domain lapses and its mail is refused until the records are back; check them again from the page.

Business includes one domain, and Team Business one per team. Each extra domain is $3 a month, renewed with your plan: set the number under Extra domains on the same page. Domains added mid-period are charged in full and count at once; fewer apply at the renewal. Domains past what the plan allows stay listed, but mail to the most recently verified ones is refused. When the account leaves Business, or the team's Team Business ends, its domains stop receiving mail and extra domains end with it.

An address at a verified domain cannot be used to sign up for an account, because its mail can be read here.

The records and the routing at a glance: Your own domain on mailhooks.cc.

Send a test email

Open Send in the endpoint bar and pick Send test email. A sample message with a code and a link is delivered straight to the endpoint, so you can see every part of the email view before wiring up a real sender. It counts as one request. It does not travel over SMTP, so the sender checks (SPF, DKIM, DMARC) do not apply to it, and the dashboard says so.

From code, client.emails.sendTest(slug, { tag }) in the SDK (or POST /api/send-test-email) delivers the same sample, to a tagged address if you pass a tag.

In the dashboard

Emails show up in the request list with an EMAIL badge, their subject, the sender, the tag, the number of attachments, and the size. Above the list, switch between All, HTTP and Email.

Selecting an email opens its own view:

  • Envelope: subject, sender, recipients (with the tag highlighted), when it arrived and its size.
  • Sender checks: one badge each for SPF, DKIM, DMARC and TLS, the spam score, and what the deliverability report found to fix.
  • Found in this email: the one-time code, the main link and your fields, each with a copy button.
  • Tabs: JSON (the parsed email, shown first), Preview (when the email has HTML), Text (with Reply only when the email quotes an earlier one), Headers, Attachments (when there are any), Authentication, Raw, and Deliveries when forwarding is on.
  • Download .eml: saves the original message, when it was stored in full.

Notes, pinning, compare, export, search, and live updates work for emails the same way as for HTTP requests.

The preview

The HTML is shown in a sandboxed frame on a white sheet, the way a mail client would show it:

  • No scripts run, and forms cannot be submitted.
  • Links in the preview do not open. Copy them from Found in this email or the Text tab.
  • Remote images are blocked until you click Load images, because loading them tells the sender that the email was opened, and from which IP address. Images written into the HTML as data: URLs show right away. Images attached to the message (cid: references) show as broken; download them from the Attachments tab.
  • Switch between a desktop and a mobile width to check a responsive layout.

For signup and login tests, the code or link is usually all you need. The dashboard picks out:

  • Codes: 4 to 8 digits (also split in two, like 123 456) or a short mix of letters and digits, but only when a word such as "code", "verification", "PIN" or "sign in" sits next to it. Order numbers, prices, years and phone numbers stay out.
  • Links: links that look like the thing the email asks you to click (confirm, verify, reset, sign in, accept an invite) come first. Unsubscribe, preference, tracking and social links are left out.

This runs in your browser when you open an email, and on the server when the API is asked for it (extracts=true); nothing extra is stored. To hide it for an endpoint, for example one that receives mail you would rather not have scanned, open Settings, then Receiving, and turn off Show codes and links found in emails. Only the endpoint's owner can change this, because it also decides whether forwarded emails carry their codes and links.

Fields you pick out

Name a value and say where it is, and every email the endpoint receives shows it, carries it in its JSON as fields, and hands it to forwarding: templates read it as {{ fields.invoice }}, and a destination can send only the emails whose field matches. Open Settings, then Receiving, and under Your fields add one:

NameFromPatternFinds in "Invoice INV-2041, total 42.00 EUR"
invoiceSubject(INV-\d+)INV-2041
totalTexttotal ([\d.]+) EUR42.00
mailerA header.+ (header X-Mailer)the X-Mailer header

A field reads the subject, the text, the HTML as text, the sender, the recipients (To and Cc), or one header, and its pattern is a regular expression whose first group is the value (the whole match without a group). Each row shows what it finds in the newest email as you type, so a pattern is right before you save it. A field that finds nothing is null.

Patterns are RE2: they run in time linear in the email, whatever a sender writes, so lookahead, lookbehind and backreferences are not available; match the text around the value and put the value in a group instead. An endpoint has up to 20 fields, each pattern up to 300 characters; a field reads the first 64 KB of its part, and a pattern with very large repeat counts (such as [a-z]{500}) is refused, because it would be slow on every email. Like codes and links, they are found when an email is shown or forwarded, and nothing extra is stored. Only the owner can change them.

For an endpoint that receives several kinds of mail, layouts give each kind its own fields, turn lists into line items and parse attachments.

Reply text

When an email quotes an earlier message (a reply), replyText in its JSON holds only what the sender wrote above the quote, without their signature, the way inbound mail services ship it (Postmark's StrippedTextReply, Mailgun's stripped-text). It is null for an email that quotes nothing. The Text tab shows it with Reply only. The quote is recognised by the line clients write above it ("On ... wrote:" and its translations, Outlook's header block, "Original Message" and forwarding separators) or a closing run of > lines.

Authentication

The Authentication tab explains each check our mail server ran when the message arrived:

CheckWhat it tells you
SPFWhether the sending server is allowed to send for the envelope sender's domain.
DKIMWhether the message carries a valid signature, and for which domain.
DMARCWhether SPF or DKIM passed for the domain in the From address, and its policy.
Reverse DNSWhether the sending server's IP address has a matching host name.
ARCFor a forwarded message (a mailing list, a forwarding rule): whether the servers it passed through sealed what they received, so the original results can still be trusted.
TLSWhether the message arrived over an encrypted connection, and with which cipher.

Failing a check does not stop a message from being captured. The results are there so you can see what a real inbox provider would think of your mail before you ship it.

Deliverability report

Below the checks, the Authentication tab lists what a mailbox provider such as Gmail, Yahoo or Outlook would hold against the email, each item marked as fine, worth improving, or to fix:

  • Domain setup, from the sender's DNS as it was when the email arrived: whether there is exactly one SPF record, how it ends (-all, ~all, ?all or +all), how many DNS lookups it takes with its includes (SPF fails past 10), and includes that name a domain without SPF; the DMARC policy that applies to the From domain (none only monitors, and a subdomain follows the record's sp), whether the record can be read at all, whether reports are on, and whether SPF or DKIM aligns with the From domain; each DKIM key's type and size (1024-bit RSA works but 2048 is recommended) and SHA-1 signatures; the ARC chain of forwarded mail.
  • Sending server: reverse DNS, whether the name the server introduced itself with (HELO) resolves to it, TLS, and a BIMI record for a brand logo.
  • Message: From, Message-ID, Date and Subject; a plain-text part beside the HTML; one-click unsubscribe (List-Unsubscribe with List-Unsubscribe-Post), which Gmail and Yahoo require for bulk mail; links whose text names a different site, link shorteners and links to IP addresses; mostly-image emails; and HTML over the 102 KB at which Gmail clips a message.

The badge in the email header sums it up (2 to fix, 3 to improve, or Deliverability when nothing is left). Like the spam score, it never decides whether mail is captured. Links are never opened to check them, because opening a sign-in or confirmation link would use it up. The domain setup is looked up as each email arrives, so emails captured before the report existed show only the message checks.

Spam score

Emails are also scored by rspamd, the spam filter many mail servers run, as they arrive. The Authentication tab shows the score, the score at which rspamd would reject the message, what it would suggest doing (nothing, marking it as spam, or rejecting it), and the rules that weighed most, such as SUBJ_ALL_CAPS or MISSING_DATE. The badge in the email header reads Not spam, Looks spammy or Spam with the score.

The score is only information. Every email that reaches an address is captured, however it scores, because test mail often looks like spam, and seeing why is the point. Scores come from the message's content, headers, links and authentication; no third-party blocklists are queried. An email has no score when rspamd could not be reached or took too long as it arrived (the tab says why), when it was captured before spam scoring was added, or when it is the dashboard's Send test email.

What is stored

PartStored
Subject, addresses, datesYes
Text and HTML partsUp to 256 KB each; longer parts are cut and marked
HeadersAll of them, as they arrived
AttachmentsName, type, size and contents (downloadable from the Attachments tab)
The original messageYes, the exact bytes, up to the 10 MB message limit
Sender check results, TLSYes

The original message and the attachments are kept in object storage in the EU (Denmark), apart from the request itself, and deleted with it. Download .eml and the Raw tab show the exact message whatever its size; each attachment has a Download button.

Emails follow the same retention as HTTP requests on your plan, or an endpoint's own shorter one (see Retention).

Quota and limits

An email counts as one request against the same quota as your webhooks, once per endpoint it is addressed to. The account page shows how much of your usage was email.

Messages are accepted or refused while the sender is still connected, so a sender always learns what happened:

SituationReply
Captured250 Message captured
No endpoint with that slug550 5.1.1 Mailbox does not exist
Guest endpoint550 5.7.1 Email capture needs a webhooks.cc account endpoint
Request quota used up552 5.2.2 Mailbox full
Message larger than 10 MB552 5.3.4 Message too big
More than 20 recipients in one message452 for the extra recipients
Temporary problem on our side451, and the sender tries again later

A message the sender retries after a temporary failure is captured once, not once per attempt. Sending the same message again on purpose captures it again.

mailhooks.cc only receives. It never sends mail, replies, or bounces of its own.

Forward to your server

Forwarding posts every email an endpoint receives to your own URL, by default as the JSON from the JSON tab, signed with Standard Webhooks headers and retried for up to a day until your server answers 2xx. A destination can also send only some emails (by subject, sender, tag, spam score, sender checks or one of your fields), send only the fields you pick as your own JSON, or post a message to Slack or Discord. Use it to handle inbound email in your app without running a mail server or parsing MIME.

Test an inbound email handler

If your app already receives email through Postmark, SendGrid, Mailgun or CloudMailin, a destination can post captured mail in that provider's shape, so the handler you wrote for it runs against real messages from real clients, with their quoting and encodings. whk tunnel does the same on localhost, and render_email in the MCP server hands one email to a local handler.

Notifications

A Slack or Discord destination posts emails too. The standard message payload has "method": "EMAIL", the recipient address as path, and the subject followed by the start of the text as preview.

In the SDK

The TypeScript SDK builds the address, waits for the email, and picks out the code. Use a fresh tag per test run, so each run finds its own email:

import { WebhooksCC, extractCode } from "@webhooks-cc/sdk";
 
const client = new WebhooksCC({ apiKey: process.env.WHK_API_KEY! });
 
const runId = `signup-${Date.now()}`;
const address = client.emails.address("my-endpoint", runId);
// my-endpoint+signup-1791450602000@mailhooks.cc
await signUp(address); // your app sends the confirmation email
 
const email = await client.emails.waitFor("my-endpoint", { tag: runId });
const code = extractCode(email); // "482913"

waitFor polls for up to 60 seconds by default and only takes mail sent to that tag. extractLink(email) returns the confirm, verify, reset, or sign-in link the same way. client.emails.get(id) adds your fields as emailFields and the reply as replyText, and client.emails.toInbound(email, "postmark") builds an email in a provider's shape to post to your handler. client.emails also lists emails, filters them by subject, sender, or recipient, and sends test emails, and client.forwarding manages forwarding. See Emails in the SDK reference, and Testing emails for test patterns.

In the API

The REST API returns emails alongside HTTP requests. An email has method set to EMAIL, path set to the recipient address, kind set to email, the full original message (or its headers, for messages over 1 MB) as body, and the parsed message as email. Add kind=email to the request list or search to get only emails, or kind=http to leave them out. Add extracts=true to the request lists or a single request to get the codes and links found in each email as extracts, and your fields and the reply text as emailFields and replyText, so a test in any language can read them without parsing the message (see Get single request). POST /api/requests/{id}/render returns an email in a provider's shape (Render an email).