MacroReply documentation

Widget · email · API — one inbox, and a loop that closes

TX 00·Overview

MacroReply collects what your customers tell you, helps your team act on it, and closes the loop when something is done about it.

Requests arrive three ways — a widget you paste into your site, an email address of your own, or the API. They land in one inbox with a type, a priority, an owner and a status, where your team answers them, adds notes customers never see, and turns bugs into GitHub issues. Closing that issue marks the request resolved and tells the person who reported it.

Around that: response-time targets with overdue alerts into Slack; a portal, under your own name and address, where customers sign in and see their requests, with an administrator per organisation who sees everything their company filed; answers drawn from your own help centre before a question ever becomes a ticket; and a view of what people keep asking for, ranked.

Where to start

  1. 01

    Put the widget on your site

    One script tag — Install, below. Feedback starts arriving immediately.
  2. 02

    Decide how you want to be reached

    Add your email address as a second channel, and set response targets so anything overdue is visible.
  3. 03

    Connect where your team already works

    Slack for triage, GitHub for bugs — each takes a few minutes.
  4. 04

    Give your customers a way in

    Turn on the portal and send mail from your own address, so everything your customers see carries your name rather than ours.

Here is the path a single message takes, end to end:

w.jsvisitor picks a type and submitsPOST /api/ingestkey resolved · body, meta and pageUrl sanitizedmessages/<id>stored · status=new · ticket token mintednotifyNewMessageteam email · Slack webhook · submitter confirmation/apptriage — set type, priority, statusaddReply fromTeamthe team answers · submitter is emailed the thread/t/<token>submitter reads and replies with no accountstatus done → opena submitter reply reopens a closed ticket
TX 02·Email

Once your project has an address in Settings, it also has an email address — something like acme-k7f2q9@in.macroreply.com. Anything sent there becomes a request, exactly like a message through the widget.

The random part is deliberate. A guessable address is a spam target, and would let a stranger file requests into your inbox. It is fixed for the life of the project, so renaming the project never breaks a forwarding rule you set up months earlier.

Use your own address. Forward support@yourcompany.com to it. One rule in your mail provider, no DNS change, and if you ever turn the forward off your own inbox carries on as before.

Replies find their way back. Every email we send about a request has a reply address carrying that request's own identifier, so when someone presses Reply it lands on the same request rather than opening a new one. Mail that carries no identifier starts a new request — putting a message on the wrong request would bury one customer's problem inside another's.

Quoted history and signatures are trimmed so a long thread stays readable. Automatic mail is never answered: out-of-office replies, bounces, mailing lists and bulk mail are recognised and ignored, which is what stops two mail systems replying to each other forever.

TX 03·Identify the sender

If you already know who is on the page, tell the widget and the email field disappears — the message arrives attributed, and your reply reaches them without anyone typing an address. Set the global before or after the script tag; it is read fresh each time the panel opens, so single-page apps can set it once the user resolves.

attribute feedback to a signed-in user

window.MacroReplyUser = {
  email: "ana@customer.com",
  name:  "Ana Rojas",
  meta:  { plan: "pro", accountId: "a_8813" }
};

meta is yours to use — plan, tenant, app version, last route. It is stored with the message and shown in the inbox, which is usually the difference between a bug report you can act on and one you have to chase. For a plain HTML site, data-mr-email and data-mr-name on the script tag do the same job.

TX 04·Appearance

The launcher is configured per project in the app and fetched by the widget at load, so changes apply everywhere without touching the customer's site again.

SettingWhat it controlsDefault
buttonColorLauncher background#7c3aed
buttonTextColorLauncher label or icon#ffffff
widgetPositionbottom-right, or a mid-right edge tabbottom-right
widgetAppearancetext label, or a chat icontext
buttonLabelText on the launcherFeedback
panelHeadingHeading inside the open panelSend us feedback
placeholderMessage field placeholderWhat's on your mind?
thanksTextConfirmation after sendingThanks!

The four copy fields cap at 80 characters and fall back to the default when cleared, so a blank field can never ship an unlabelled button.

TX 05·Answers

Point the widget at your MacroPrimer workspace and it searches your help centre as someone types, offering matching articles before they send. Many questions never become tickets — which is the cheapest possible way to answer one.

Not switched on yet. This MacroReply instance is not connected to MacroPrimer, so the setting below has nothing to search.

Connecting your help centre

  1. 01

    Find your MacroPrimer workspace name

    It is the first part of your help centre address — for acme.help.macroprimer.com the workspace is acme.
  2. 02

    Enter it under Widget

    Settings → Widget, in Answer from your help centre. Clearing the field turns it off again.
  3. 03

    Type a question into your own widget

    Matching articles appear after a few characters. Nothing appears if your knowledge base has not been built yet, or if no article matches — silence is deliberate, since a confident wrong answer is worse than none.

Only public knowledge is used. Anything marked internal in MacroPrimer never leaves it, and a workspace with no knowledge base yet simply shows nothing.

A question with no answer is worth more than the answer. When the search finds nothing, the question is recorded as a documentation gap. Those accumulate into a ranked list of what your help centre fails to cover, measured by how many people hit it — which is the list worth writing from next.

Search runs on the exact words in your articles, so it never invents an answer. A question it cannot match returns nothing rather than a confident guess, and the widget behaves exactly as it does today.

TX 06·Triage

Every message arrives with a type chosen by the sender, a status of new, and a priority of normal. Your team sets the rest.

FieldValuesSet by
typebug · feature · question · otherthe sender, your team can correct it
statusnew · open · waiting · doneyour team
prioritylow · normal · high · urgentyour team
assigneeany member of the projectyour team

The widget offers Bug, Idea and Question; other is what the API records when a message arrives without a recognised type. Correcting a type is recorded — the corrections are what make classification better over time, so they are kept rather than overwritten.

waiting means the ball is in the customer's court. It matters beyond tidiness: it is what stops the response-time clock, so time spent waiting on a reply you asked for is not counted against you.

TX 07·Rules

Decide how a request is filed before anyone reads it. A rule watches for something — the type, how it arrived, words in the message, the sender's domain, the organisation they belong to — and sets a priority, a type, a status or an owner.

Rules run once, when the request arrives, and never again. Anything you change afterwards stays changed: a rule cannot reach back and undo a person's decision. Rules apply in order, and a later one wins where two set the same thing, so the list reads top to bottom like a sentence.

A rule with no conditions never matches — a half-built rule must not quietly apply to everything that arrives. And a rule can park a request or move it along, but never resolve it: closing something nobody has read is not triage, it is losing it.

If rules cannot be read for any reason, the request is stored anyway, unrouted. Losing a bug report to a broken rule would be a poor trade.

TX 08·Response times

Set a target per priority in Settings — how long a request may wait for its first reply, and how long until it's resolved. Overdue requests are flagged in the inbox and collected in the Breaching queue, alongside Unassigned and Mine.

The clock stops while you're waiting on the customer. Time a request spends in waiting is subtracted from resolution time, so asking a question and waiting three days for an answer doesn't count against you. First-reply time has no such adjustment — a request can't be waiting on the customer before anyone has written to them.

Only a reply the customer can actually read starts the clock. An internal note is not a first response, and Reports shows median and p90 by assignee, priority and organisation.

Two things are deliberately not counted. With no targets set, nothing is marked overdue and reports show real times without judging them — rather than showing everything as on track. And a request closed without any reply is listed separately rather than counted as a fast response, because the customer never heard from you.

TX 09·Outcomes

Response time says how fast you answered. It does not say whether anything came of it. Every request carries an outcome instead:

OutcomeMeansCounts as resolved
ShippedA linked issue closed and the person was toldyes
AnsweredResolved, and a reply had reached themyes
Closed unansweredResolved without anything reaching themno
OpenStill in flightno

An issue link alone is not shipped. An open issue is a promise, not an outcome — it counts only once the issue closes and the person who asked has been told.

The headline figure is the share that reached an outcome. Closing tickets moves your response time; only this moves that number, which is the point of measuring it. If it stays flat while volume grows, requests are being closed rather than answered.

The customer's own verdict

Once a request has been answered or resolved, the person who sent it is asked, on their own ticket page, whether it sorted things out. Both answers count as confirmation — “you did not fix it” is a recorded outcome, and counting only the praise would flatter the number. Satisfaction is shown as a share of the requests that were actually rated, never of everything, because dividing by silence makes a good score look bad and hides a bad one.

TX 10·Signal

The same feedback the inbox holds, read as a list of things to build rather than a queue to clear. Requests about the same thing are grouped, ranked by how many people asked, with how many of those arrived in the last 30 days so a rising theme is visible.

Grouping is literal. It matches the words people actually used, so it never invents a theme nobody asked for and it gives the same answer twice — which matters when you are deciding what to build. Requests asked only once are counted but not listed; a theme appears once two people have asked for the same thing.

Underneath sits the other half: questions your help centre could not answer, ranked by how many people hit them. Feedback tells you what to build; that list tells you what to write.

How you compare

Where enough products are using MacroReply, the reports page shows the typical first response, resolution and outcome rate across them. Medians across products rather than across requests, so one very large customer cannot set everyone's benchmark. Nothing identifying any individual product is shown or stored — and no comparison is published at all until enough products contribute that a median cannot point at one of them.

TX 11·Replies

Each message gets an unguessable token when it is stored, and that token is a private page where the sender can read the thread and write back — no password, no account, no login wall between a customer and the answer they asked for.

When your team replies, the sender is emailed the reply and the link. When the sender replies, your team is alerted, and a ticket already marked done returns to open — because someone writing back is the clearest possible signal that it wasn't finished.

Internal notes. Tick Internal next to the reply box and the note stays in your inbox: it is not emailed, and it never appears on the sender's ticket page. Notes are marked in amber in the thread, and the toggle resets after every send, so a note is always a deliberate act. Only public replies count as your first response.

TX 12·Attachments

Senders can attach screenshots and files: paste an image straight into the message box, drag one onto the panel, or pick a file. Attachments appear on the message in your inbox and on the sender's own ticket page.

RuleDetailLimit
TypesPNG, JPEG, GIF, WebP, PDF, plain text6 types
Per fileLarger files are refused, not truncated5 MB
Per messageExtra files are dropped5 files
UploadsPer widget key, per hour60

The file type is decided by reading the bytes, not by what the browser claims — a file named shot.png that is really something else is refused. Stored files are served from a separate storage domain rather than from MacroReply, so an uploaded file can never run code against your dashboard, and everything except an image is sent as a download rather than opened in the browser.

The URL of a stored file is long and unguessable, but anyone holding it can open it without signing in — that is what lets an image preview inline in an issue tracker later. Treat an attachment link like the file itself.

TX 13·Notifications

Three switches, per project.

SettingEffectDefault
notifyOnNewEmails every project member on new feedback and on customer replieson
confirmSubmitterEmails the sender a receipt and their ticket linkon
slackWebhookUrlPosts new feedback and customer replies to a Slack channeloff

Slack takes an incoming webhook from api.slack.com — the URL must begin https://hooks.slack.com/services/. Leave it blank to turn Slack off. Every notification is best-effort: a failed send is logged and never blocks the message from being stored.

TX 14·Slack

New requests arrive in a channel as a card your team can act on, and anything that goes past its response target raises an alert there.

Not switched on yet. The steps below are what connecting will look like; the MacroReply Slack app isn't published, so there is nothing to install today.

Connecting Slack

  1. 01

    Open Settings → Notifications

    You'll need to be an administrator of the project. Whichever project is selected is the one being connected — check the switcher at the top of the sidebar first.
  2. 02

    Press Add to Slack

    Slack asks which workspace to install into. If you belong to more than one, pick it from the dropdown in the top right of Slack's page.
  3. 03

    Choose the channel

    On the same Slack page, pick where requests should be posted. A private channel works, but you must invite the app to it afterwards with /invite @MacroReply — Slack cannot post to a private channel it isn't in.
  4. 04

    Press Allow

    MacroReply asks only to post messages. Slack returns you to Settings, which now names the workspace and channel it is connected to.
  5. 05

    Set response targets, if you want the alerts

    Under Response times. Without targets nothing is ever overdue, so the alert never fires — the cards still arrive.

What your team can do from Slack

Each card carries the request, who sent it and the page it came from, with buttons to mark it Working on it or Resolved and a link into the inbox. Pressing a button updates the request straight away; only the person who pressed it sees the confirmation, so the channel isn't filled with acknowledgements.

The card never carries the reporter's private ticket link. A channel is much wider than your inbox, and that link lets whoever holds it reply as the customer.

If something doesn't work

What you seeUsually meansFix
Nothing arrives in the channelThe app isn't in a private channel/invite @MacroReply
Cards arrive, no overdue alertsNo response targets are setSettings → Response times
The same request twiceAn old incoming webhook is still setClear it; the app supersedes it
Buttons do nothingThe app was removed from SlackReconnect from Settings

Removing the app in Slack stops everything at once. If you were using an incoming webhook before, it keeps working and switches off by itself once the app is connected, so the same request never arrives twice.

TX 15·Team & roles

A project has members. An admin can invite people, edit the widget and notification settings, rename the project and delete it. A member reads the inbox and replies. Inviting an address that has never signed up creates their account, and they are emailed to say so — Google sign-in is how they get in.

TX 16·GitHub

Turn a bug report into a GitHub issue in one click — and when you close that issue, the person who reported it hears about it automatically.

Not switched on yet. The steps below are what connecting will look like; the MacroReply GitHub App isn't published, so there is nothing to install today. Everything else on this page works now.

What it does. Every request in your inbox gets a Create issue button. The issue opens in the repository you choose, carrying the report, the page it came from, any context your site attached, and the screenshots — images embedded so they preview inline. The request keeps a link to the issue, and the issue links back to your inbox.

Closing the issue closes the loop. Mark it closed on GitHub and the request is marked resolved here, and the person who reported it is emailed to say the thing they asked for has shipped. Reopen the issue and the request reopens with it. You never have to remember who to tell.

Connecting a repository

  1. 01

    Install the MacroReply app on your repositories

    From Settings → GitHub, follow the install link and pick the repositories MacroReply may open issues in. You can choose a single repo; it never sees anything you don't select, and you can change or revoke the selection on GitHub at any time.
  2. 02

    Copy the installation ID

    After installing, GitHub sends you to a page whose address ends in /installations/12345678. That number is the installation ID.
  3. 03

    Paste it into Settings → GitHub

    Enter the installation ID and the repository as owner/name — exactly as it appears in your repository's address. The panel confirms when the connection is live.
  4. 04

    Create your first issue

    Open any request and press Create issue. The button is replaced by a link to the issue. Close that issue on GitHub and watch the request turn resolved here a moment later.

What is sent to GitHub

Only what you would put in the issue yourself: the message, the reporter's name or email if they gave one, the page they were on, the context your site attached, and links to any files they uploaded. Attachment links are long and unguessable but not password-protected, so anyone with the issue can view them — which is what lets a screenshot preview inline.

What is never sent. The reporter's private ticket link stays out of the issue. That link lets whoever holds it write replies as that person, so it never goes anywhere your repository collaborators could read it. Internal notes stay in your inbox too.

If something doesn't work

What you seeUsually meansFix
Create issue is missingNo repository connected for this projectSettings → GitHub
Couldn't create the issueThe app was removed from that repository, or the name is wrongRe-install, check owner/name
Issue closed, request still openThe app isn't receiving issue eventsRe-install the app
Reporter wasn't emailedThey never gave an email addressNothing to fix

Removing the app on GitHub stops everything immediately. Requests keep their issue links, and nothing already created is deleted.

TX 17·Customer portal

A page where the people who write in can sign in and see their own requests. Give the portal an address in Settings and it goes live at that address; clear the address and it switches off. Set a heading, an accent colour, and optionally remove the MacroReply footer — the portal wears your branding, not ours.

Email from your own address. By default everything your customers receive comes from MacroReply. Set a sender address in Settings and we hand you the DNS records to publish; once your provider can see them, your customers hear from you instead. Until then mail keeps going out from MacroReply — sending from a domain whose records are not live yet lands in spam and damages that domain, so it is never done optimistically.

A subdomain such as mail.acme.com keeps this separate from the rest of your email. Turning off the MacroReply footer clears it from the portal, the emails your customers receive, and their ticket page.

Signing in. Customers get a one-time link by email — no password to choose, forget or reset. The link works once and expires after 15 minutes. Asking for a link always says the same thing whether or not the address has ever written in, so the form can't be used to find out who your customers are.

Organisations. Group customers by their email domain, then appoint an administrator who can see every request their company filed. Shared providers like gmail.com are refused: they identify a mailbox, not a company, and treating one as an organisation would show unrelated people each other's requests.

Administrators are appointed by you. There is deliberately no way for a customer to promote themselves — signing in proves who someone is, never what they may read. And the portal shows only the request, its status and its attachments: priority, assignee, internal notes and the metadata your site attached all stay in your inbox.

TX 18·Customer lookup

Show your own data beside a request. Point MacroReply at an endpoint of yours and, when someone opens a request, a panel appears with whatever your API answers — their plan, their account state, how much they owe you.

Setting it up

  1. 01

    Give it an endpoint

    In Settings → Developers, set an endpoint like https://api.yourcompany.com/customers?email={{email}}. It must be https and must not resolve to a private network.
  2. 02

    Fill in what it needs

    {{email}}, {{name}}, {{orgId}}, {{contactId}}, and {{meta.anything}} for the context your widget attaches. Values are URL-encoded, so one cannot bolt an extra parameter onto the request.
  3. 03

    Add a credential if you need one

    An optional header, written as Header-Name: value, is sent with every lookup. Use a read-only credential — it is stored on our side.
  4. 04

    Open a request

    Press Look up this customer and the panel fills in.

What your endpoint should return

A flat JSON object. Top-level values are shown as rows, one level of nesting is flattened to account.tier, and an array is summarised as a count rather than dumped. Empty values are skipped, at most 40 rows are shown, and long values are truncated. Everything renders as plain text, never as markup.

a good response

{
  "plan": "Enterprise",
  "seats": 240,
  "renews_on": "2027-01-31",
  "account": { "tier": "gold", "owner": "Marta Silva" },
  "open_invoices": [ 1, 2 ]
}

Why some endpoints are refused

Requests are made from our servers, so an endpoint that points inside a private network — a loopback address, a company intranet, or a cloud metadata service — is refused. That check runs against the addresses your hostname actually resolves to, not the name, and every address is checked rather than just the first. Redirects are not followed, because a permitted host could otherwise redirect us somewhere that isn't. Lookups time out after a few seconds and the response size is capped, so a slow or enormous reply never holds up your inbox.

Lookups happen when someone opens a request, not when it arrives. A plan captured at ingest would be a stale snapshot dressed up as current.

TX 19·API

The widget is a client of the same public endpoints you can call yourself — to forward feedback from a native app, a CLI, or a server you already run.

POST /api/ingest — create a message

curl -X POST https://macroreply.com/api/ingest \
  -H "Content-Type: application/json" \
  -d '{
    "key":     "mr_your_project_key",
    "type":    "bug",
    "body":    "Export to CSV fails on invoices over 500 rows.",
    "email":   "ana@customer.com",
    "name":    "Ana Rojas",
    "meta":    { "plan": "pro", "appVersion": "3.2.1" },
    "pageUrl": "https://app.example.com/invoices"
  }'
ResponseMeaningBody
201Stored, notifications dispatched{ "ok": true }
400Missing key or empty message{ "error": … }
401No project matches that key{ "error": … }

To attach a file from your own code, POST it to /api/upload with the same widget key first; the response hands back an attachment record and a signature. Pass those straight through in an attachments array on the ingest call — the signature is checked, and an attachment without a valid one is dropped rather than stored.

Only key and body are required. An unrecognised type becomes other rather than an error, so a bad enum never costs you a customer's report. The endpoint is CORS-open because the widget runs on your domain, not ours.

POST /api/ticket/reply — reply as the sender

curl -X POST https://macroreply.com/api/ticket/reply \
  -H "Content-Type: application/json" \
  -d '{ "token": "<ticket token>", "body": "Still failing on 512 rows." }'

GET /api/widget-config — the launcher's own config

curl "https://macroreply.com/api/widget-config?key=mr_your_project_key"
TX 20·API reference

A REST API for reading and updating requests from your own systems — a dashboard, a script, an automation tool. Everything the inbox does, it does through the same data.

Authentication

Create a key in Settings → Developers. It is shown once, at creation, and never again: only a hash of it is stored, so we cannot show it to you a second time and nobody can take a working key from our database. Lose it and you revoke it and make another.

A key is scoped to the project it was created in. It cannot see or change anything in another project, and asking for a request that belongs to one answers 404 rather than 403 — the API never confirms that an id exists somewhere you cannot reach.

every request

curl https://macroreply.com/api/v1/messages   -H "Authorization: Bearer mrk_live_your_key_here"

Rate limits and errors

StatusMeaningWhat to do
200 / 201Success
400A field or filter value was not recognisedFix the value; nothing was changed
401Missing, malformed, revoked or unknown keyCheck the Authorization header
404No such request in this projectCheck the id and the key's project
429Over 600 requests in an hour, per keyBack off and retry

An error body is always { "error": { "status", "message" } }. Unknown filter values are an error rather than being ignored — silently returning everything because someone misspelled a status is how a script changes the wrong things.

The request object

shape returned everywhere

{
  "id": "kP2n…",
  "type": "bug",              // bug · feature · question · other
  "status": "open",           // new · open · waiting · done
  "priority": "high",         // low · normal · high · urgent
  "channel": "widget",        // widget · email · api · unknown
  "body": "Export to CSV fails on invoices over 500 rows.",
  "from": {
    "name": "Ana Rojas",
    "email": "ana@acme.com",
    "contact_id": "c_…",
    "organization_id": "o_…"
  },
  "page_url": "https://app.example.com/invoices",
  "metadata": { "plan": "pro", "appVersion": "3.2.1" },
  "attachments": [
    { "id": "…", "name": "shot.png", "content_type": "image/png", "size": 84213, "url": "https://…" }
  ],
  "assignee_id": null,
  "github_issue_url": null,
  "created_at": "2026-09-09T14:03:11.000Z",
  "first_responded_at": null,
  "resolved_at": null
}

The customer's private ticket link is deliberately absent. It is their access grant — whoever holds it can post replies as them — and no integration needs it.

List requests

GET /api/v1/messages

curl "https://macroreply.com/api/v1/messages?status=new&priority=urgent&limit=20"   -H "Authorization: Bearer mrk_live_…"

# → { "data": [ …request objects… ], "has_more": false, "total": 3 }
ParameterValuesDefault
statusnew · open · waiting · doneall
prioritylow · normal · high · urgentall
typebug · feature · question · otherall
channelwidget · email · apiall
limit1–20050

Create a request

POST /api/v1/messages

curl -X POST https://macroreply.com/api/v1/messages   -H "Authorization: Bearer mrk_live_…"   -H "Content-Type: application/json"   -d '{
    "body":  "Billing page 500s on annual plans.",
    "type":  "bug",
    "email": "ana@acme.com",
    "name":  "Ana Rojas"
  }'

Only body is required. The request is recorded with channel api, and your rules apply exactly as they do to anything arriving through the widget or by email — the door does not change the routing.

Read one request, with its thread

GET /api/v1/messages/{id}

curl https://macroreply.com/api/v1/messages/kP2n…   -H "Authorization: Bearer mrk_live_…"

# → { "data": { …request…, "replies": [ … ] } }

Update a request

PATCH /api/v1/messages/{id}

curl -X PATCH https://macroreply.com/api/v1/messages/kP2n…   -H "Authorization: Bearer mrk_live_…"   -H "Content-Type: application/json"   -d '{ "status": "done", "priority": "high", "assignee_id": "acct_…" }'

Any of status, priority, type and assignee_id. Changing the type is recorded as a correction, exactly as if someone had re-typed it in the inbox — those corrections are what make classification better over time, so they are kept rather than overwritten.

Reply to a request

POST /api/v1/messages/{id}/replies

curl -X POST https://macroreply.com/api/v1/messages/kP2n…/replies   -H "Authorization: Bearer mrk_live_…"   -H "Content-Type: application/json"   -d '{ "body": "Fixed in 3.2.2 — thanks for the report.", "author": "Ana" }'

A reply is public by default and emails the customer. Pass "visibility": "internal" for a note that stays in your inbox. The default is public on purpose: an integration that forgets the field should send the customer an answer rather than quietly file a note nobody reads.

TX 21·Limits

Anything longer is truncated, not rejected — a message over the cap still reaches you.

FieldRuleLimit
bodyTruncated5,000 chars
emailTruncated320 chars
nameTruncated200 chars
pageUrlDropped unless http(s)1,000 chars
metaExtra keys dropped20 keys
meta keyTruncated64 chars
meta valueNested objects dropped500 chars
TX 22·Security

The widget key is public page source, so every field arriving at ingest is treated as hostile. Metadata is flattened to bounded strings and keys that could poison an object prototype are discarded. A pageUrl is kept only if it parses as http or https, so a javascript: URL can never become a clickable link in your inbox.

End users on your site are told, in the widget itself, that their message goes to MacroReply, with a link to the privacy policy. That notice is not configurable — they have no other way to know a third party receives what they type.


— — — end of message — — —

Something missing? Start free and send it through the widget — it arrives in our own inbox.