Documentation menu

Responses & webhooks

Everything recipients send back — poll votes, form submissions, ratings, spins — is collected per project. It flows back to you two ways: a per-recipient responses view in the dashboard, and an optional webhook that pushes each interaction to your own endpoint the moment it arrives.

Per-recipient responses in the dashboard

Open Dashboard → your email → Responses. Every interaction the email has collected is grouped by recipient and joined with the project's data source. Each group shows who answered — their email address and the other fields from their row — alongside what they did: which block, which action, the submitted values, and when.

Attribution works through the same signed tokens that protect live view links. When an email is personalized from a data source, each recipient's links carry a token bound to their row, and interactions arriving with a valid token are attributed to that row. Interactions that arrive without a token (for example a poll vote from an email sent without personalization) still count — they're listed in a separate Anonymous group.

The raw event list is paginated (newest first): once you've loaded the latest page, a Load more action fetches older events. Everything below — rollups, histograms, the funnel and trend — isn't affected by that pagination; it's precomputed and always covers the project's full history.

Orders, fulfillment and refunds

Any project with a product block gets an Orders table on its Responses page — one row per checkout, with the buyer, amount, status and placement time.

  • The moment a payment clears, the buyer is emailed automatically: a digital product's delivery content, or a confirmation that a physical order is on its way to fulfillment.
  • For physical orders, click Mark fulfilled once you've shipped it — MailInApp doesn't handle shipping itself, but it does email the buyer a shipped notice the moment you flip the switch.
  • Click Refund on any paid order to reverse the charge through your connected Stripe account; the buyer is emailed a refund confirmation. The same email goes out automatically if a sale clears right as the last unit sells out — MailInApp refunds the buyer rather than silently over-selling.
  • Contact buyer opens a support ticket prefilled with that order's context — the quickest way to ask a buyer something directly.

Purpose-driven headline metric

If a project has a purpose set (survey, promotion, newsletter or event), the Responses page leads with the one number that purpose cares about most: response rate for surveys, recipients engaged for promotions, and opens tracked for newsletters and events. Transactional projects and projects with no purpose set skip straight to the funnel and rollups below. Change or clear the purpose anytime from the dropdown next to the page title; it only affects which number is featured, never what gets recorded.

Completion rate

When an email asks for answers in two or more blocks (polls, ratings, quizzes, RSVPs and forms), Insights tracks who answered all of them. The studio's Responses panel sets which blocks count (Blocks that count, every one by default). With Thank recipients set to After any answer, or fewer than two blocks counted, there's no completion rate.

  • The Overview has a Completion rate tile: the people who completed, as a share of everyone who answered at least one counted block, with its 95% interval. Click it to see who completed.
  • Where people stopped gets a Completed bar, and under it a link to the people who started but didn't finish.
  • In Recipients, the Completed column shows when each person finished, and Add condition offers Started the response and Completed the response, each of which can be turned around ("Didn't complete the response"). The wide CSV export has a completed_at column.
  • The PDF report and Explain this page include the rate, and What changed can name a move in it.

Under a date range, the rate counts the people who started inside it, and how many of them completed by its end. A response completes once, the first time every counted block it was shown has an answer, so changing an answer later doesn't change the count. Only completions recorded by MailInApp count: an email answered in full before completions were recorded shows no completions for those people.

CSAT, CES and NPS rollups

Every rating block gets a rollup above the recipient list: response count, average, and a histogram (the distribution behind the average — a 4.1 average hides whether it's all 4s or a 5s-and-1s split). NPS-style blocks show promoter/passive/detractor counts and the −100…100 score instead of a plain average. A response distribution breakdown does the same for poll blocks, by option.

An engagement funnel (Sent → Opened (approx.) → Responded) sits above the rollups. A per-block response trend chart (day or week buckets) shows the average over time — the value of a recurring survey is the trendline, not one snapshot. It only renders once a block has at least two buckets of data.

Survey question rollups

Every form block gets a per-question rollup, the same way rating and poll blocks do. Multiple choice, checkboxes, linear scale and star rating questions get a bar chart of option counts; free-text questions (short text, long text, email, number, phone, date) get a response count plus a handful of sample answers. Multi-page surveys roll up identically to single-page ones — a submission only counts once every page has been completed, so an abandoned survey never shows up as a partial response.

These poll, rating and survey-question results aren't a dead end — any of them can be plotted directly inside a future email by binding a chart block to "campaign response," no manual re-entry required. See Binding charts to real data.

Homework & gradebook

Every assignment block with at least one completion or submission gets a stat card (completions, late completions, submissions) above a Gradebook table. The table has one row per student and one column per assignment, showing "Done" or the submitted answer in green, or in red if it arrived after the block's due date. Assignments with no responses yet don't clutter the table. Like every other rollup, it's precomputed and covers a project's full history, and both CSV exports include an assignment column per block.

Click heatmap

Below the rollups, a Click heatmap ranks every plain link-bearing block — buttons, linked images, social icons, scratch-off and spin-to-win prize links — by click count, with a bar length and intensity scaled to relative volume. See how clicks are tracked for what does and doesn't count.

Scroll-depth funnel

A scroll-depth funnel shows what share of live-view visitors reached each 25/50/75/100% milestone of the page, so you can tell whether people are bailing early or reading to the end. It only reflects visits to the hosted live view, not the mailed email itself — see Scroll depth for why.

A/B testing & automatic winners

Sending with more than one variant (see Send) adds an A/B test card to the email's Insights, under Sends. Pick the send under A/B send to see each variant's open, click and response rates for that send. Each rate counts a person once, out of the people sent that variant. On the Pro plan and up, Compared with variant A adds each variant's difference from variant A, with a 95% interval.

A variant isn't limited to subject-line wording; it can also swap the sender identity, or the entire email. A variant that mailed a different email is counted in that email, under the same send, and the card links to that email's Insights.

Pick a winner automatically mails a test slice first. Choose a test fraction (e.g. 20% of the list, split across variants), how long to wait before deciding, and which metric to decide on: open, click, in-email response, or revenue. In-email response (poll, quiz or RSVP completion) is the recommended default, since Apple Mail Privacy Protection inflates opens. Deciding on revenue needs the Pro plan or higher.

Once the wait elapses, MailInApp mails the variant with the best per-recipient rate to everyone held back, never the biggest raw total. A tie or a sample under 5 sends per variant falls back to the first variant, and says so. The card's banner shows which state a test is in, and whether the leader is ahead by more than the noise so far.

Reading A/B results with care

A plain A/B send fixes nothing in advance: not the metric, not the sample size, not when to stop. Read its card as a simple comparison, as its label says, not as proof that one variant is better. For an answer you can act on, run an experiment, which handles each of these points:

  • Small samples are noisy. With a few hundred recipients per variant, a gap of one or two percentage points is often chance. The automatic winner decides from 5 sends per variant, far too few to rule chance out. An experiment sizes its sample before the send with the sample size planner, and its results say the smallest difference it could detect.
  • Each test splits the list afresh. A contact's variant depends on their email address and on the send, so two tests on the same list put different contacts in variant A. An experiment draws its sample once, gives each contact one arm, and leaves them out of the email's other sends.
  • Click rate counts people, not clicks. Each recipient counts once in the click rate, however many times they click, like the open and response rates. An experiment's unique click rate counts the same way, out of delivered emails.
  • Revenue means in-email checkout. The revenue metric counts Product block purchases only. Orders on a connected Shopify or WooCommerce store don't count toward a variant. An experiment matches store orders to its arms by discount code, tracked link or email address.
  • Opens are the weakest signal. Apple Mail Privacy Protection and image proxies load the open pixel automatically, so open rates run high and vary by email app. An experiment shows opens for context only, never as its deciding metric.
  • One send reaches up to 200 recipients. A bigger test has to be split across several sends, and results are only as clean as the timing. An experiment sends its whole sample in one job, every arm interleaved over the same minutes.

Send-time optimization

Available on Pro and above. Instead of everyone in a send going out at once, Send at each recipient's own best time looks at each contact's own open-time history — which hour of the day, in UTC, they've actually opened mail from you most. It holds their message until that hour. Anyone without enough open history yet to have a reliable signal falls straight through to an ordinary immediate send.

It applies the same way to a manual send, a recurring schedule, and a journey's own send step; a schedule still sends everyone with no signal at its own configured hour, matching pre-optimization behavior. It can't currently be combined with an automatic-winner test in the same send, since a deferred recipient wouldn't be counted by the time a winner is decided.

Breaking down by a field

Use Break down by to bucket the same rollups by any field from your data source — average CSAT per agent, NPS per plan, and so on. Recipients whose row was deleted or shrunk since sending bucket under (unknown); if a free-text field produces more than 20 distinct values, the smallest buckets fold into a trailing Other group so the view doesn't explode.

CSV export

Download CSV on the Responses page exports a wide, one-row-per-recipient file: every data-source field, first-open time, and one column per interactive block (headed by its question). Follow-up answers get their own <question> — follow-up column. A second raw events export gives one row per event (recipient, block, action, value, timestamp) for analysts who want the long format instead.

Survey lifecycle

A project can have a close date and/or a response cap (maxResponses). Once either is reached, new interaction events are rejected — the live view shows a closed notice instead of interactive blocks, though static content still displays — and the Responses page shows whether the survey is currently closed. Nothing is filtered retroactively: responses already recorded stay in your data.

In-app low-score alerts

Beyond the webhook's lowScore flag (below), a project can list up to a handful of email addresses to notify directly — no Zapier/Make step required. Whenever a rating response crosses its block's configured threshold, MailInApp emails those addresses (through your own SMTP settings) with the question, the score, the recipient's identity where known, and a link straight to the Responses page. If no SMTP relay is configured the alert is skipped silently — the webhook still fires. Alerts are capped per project per hour so a burst of low scores can't flood your relay.

Rebuilding aggregates

Rollups, the funnel and trend are served from a precomputed per-project aggregate, updated as events arrive — so they stay fast regardless of how much history a project has. If a project shows unexpectedly low or zero totals, it likely collected responses before this aggregate existed for it; click Rebuild aggregates on its Responses page once to replay its full event history into the aggregate. New projects never need this.

Webhooks

If you'd rather have the data land in your own systems — a CRM, a spreadsheet, an automation tool — configure a webhook on the same Responses page: enter an HTTPS URL and click Enable.

This webhook covers one email. To get every email's interactions, plus account events such as new leads, bookings and deal moves, from one place, use an account webhook instead. It's signed the same way.

Two things happen:

  1. You're shown a signing secret (whsec_…) — copy it immediately, it is displayed only this once. It's stored server-side and masked everywhere afterwards, like all credentials in MailInApp.
  2. From then on, every interaction is POSTed to your URL as JSON, right after it's recorded.

You can rotate the secret (a new one is minted and shown once) or remove the webhook at any time.

Payload

{
  "type": "interaction.received",
  "projectId": "abc123",
  "event": {
    "campaignId": "abc123",
    "blockId": "poll-1",
    "blockType": "poll",
    "action": "vote",
    "value": { "option": "Blue" },
    "recipient": "row:3",
    "projectId": "abc123",
    "receivedAt": 1752480000000
  },
  "recipient": {
    "key": "row:3",
    "row": { "email": "[email protected]", "first_name": "Ada" }
  },
  "lowScore": false
}
  • event.value is the collected data itself — the chosen poll option, the form's field values, the star count.
  • recipient is null for anonymous interactions. For attributed ones, key is the recipient's row index in your data source ("row:3" = fourth row).
  • recipient.row — the recipient's full data row — is included for hosted data sources. For API-type sources we don't call your endpoint on every interaction; join on the row index on your side.
  • lowScore is true when the event is a rate action on a rating block whose value falls at or below that block's configured alert threshold — the signal a Zapier/Make automation (or your own in-app alert) filters on to page a support manager. It's omitted entirely on non-rating events, or when the block has no threshold configured.

Completed responses

When a recipient has answered every block the email counts (set in the studio's Responses panel), the webhook also gets one response.completed delivery for them, signed the same way. Its data is the account webhook event's:

{
  "type": "response.completed",
  "projectId": "abc123",
  "data": {
    "projectId": "abc123",
    "projectName": "Customer survey",
    "recipient": "row:3",
    "contact": { "listId": "list_abc123", "rowId": "3", "email": "[email protected]" },
    "sendId": "send_abc123",
    "variant": null,
    "blockIds": ["poll-1", "rating-1"],
    "answers": {
      "poll-1": { "option": "Weekly" },
      "rating-1": { "rating": "9" }
    },
    "firstAnsweredAt": 1752480000000,
    "completedAt": 1752480060000
  }
}

It's sent once per recipient: changing an answer later doesn't send it again. This delivery has no event, so check type first.

Verifying signatures

Every delivery is signed so your endpoint can confirm it really came from MailInApp. Two headers are sent:

HeaderContents
X-MailInApp-TimestampWhen the delivery was signed, in epoch milliseconds
X-MailInApp-Signaturev1= followed by hex HMAC-SHA256(secret, timestamp + "." + rawBody)

Compute the expected signature from the raw request body (before any JSON parsing) and compare with a constant-time comparison. Rejecting stale timestamps blocks replayed deliveries:

import { createHmac, timingSafeEqual } from "node:crypto";

function isValidDelivery(headers, rawBody, secret) {
  const timestamp = headers["x-mailinapp-timestamp"];
  const given = Buffer.from(headers["x-mailinapp-signature"] ?? "");
  const expected = Buffer.from(
    "v1=" +
      createHmac("sha256", secret)
        .update(`${timestamp}.${rawBody}`)
        .digest("hex"),
  );
  if (given.length !== expected.length || !timingSafeEqual(given, expected)) {
    return false;
  }
  // Reject deliveries signed more than 5 minutes ago (replay protection).
  return Math.abs(Date.now() - Number(timestamp)) < 5 * 60 * 1000;
}

Delivery semantics

  • First attempt is immediate, then retried automatically. Deliveries time out after 5 seconds; anything short of a 2xx response (a timeout, a connection failure, or an error status) is treated as a failure. The event is always stored in MailInApp first, so a missed delivery loses nothing — treat the webhook as a real-time signal and the Responses view as the source of truth.
  • Automatic retries with backoff. A failed delivery is retried at increasing intervals — roughly 1 minute, 5 minutes, 30 minutes, 2 hours, then 6 hours — against your webhook's current URL and secret, so a rotated secret or updated URL is picked up automatically. If every retry still fails, the delivery stops retrying on its own, but it's never discarded.
  • Manual redeliver. Any delivery that's still failing (retrying or exhausted) shows up under Failed deliveries on the Responses page, with the reason for the last failure and a Redeliver button — useful right after you've fixed whatever was broken on your end, instead of waiting for the next scheduled retry.
  • Never in the recipient's way. Deliveries happen after the recipient's interaction is acknowledged; a slow or broken endpoint can't delay or fail their vote or submission.
  • Respond fast. Return any 2xx quickly and do heavy processing asynchronously.

Heads-up on trust: the interaction endpoints are public by necessity (an inbox can't authenticate), so anonymous events are unauthenticated by design. Attributed events are protected by signed recipient tokens. Verify the delivery signature, and treat event.value as user input.