Guide · Customer support

Running a support inbox with an agent

A real mailbox at support@yourdomain.com that an agent reads, triages, files and answers — over HTTP, with no IMAP client and no browser.

Support is the use case that justifies a mailbox rather than a webhook. A ticket is not one message — it is a conversation the agent has to be able to re-read, and often re-read days later. Read what this API can't do yet before you build: the honest constraints shape the design.

Setting it up, once

The domain half is a human in a browser. Everything after that is the agent.

01

Attach the domain

In the dashboard, add the domain and publish the TXT and MX records it shows you. That turns on inbound. Once those verify, the outbound set appears — DKIM, a MAIL FROM subdomain for SPF alignment, and a recommended DMARC record.

Sending stays disabled until the outbound records verify. That is deliberate: it stops an unauthenticated domain damaging its own reputation on the first send.

02

Hand the agent a token

Create a registration token in the dashboard — it needs a signed-in session, not an API key — and give it to the agent out of band. One token can mint a whole team of addresses.

03

The agent claims support@

Same registration call as a shared handle, plus the token and the local part it should take. The response shape is identical; only the address is different.

Claim the address, build the queues

The local part must be lowercase alphanumeric, up to 64 characters, and not a reserved name like postmaster or abuse.

Folders are how you keep state. There is no ticket status field, so a message's folder is its status.

# agent claims support@yourdomain.com
RESP=$(curl -s -X POST https://api.mailgi.xyz/v1/agents/register \
  -H "Content-Type: application/json" \
  -d '{"domainToken":"'"$TOKEN"'","localPart":"support"}')

KEY=$(echo "$RESP" | jq -r .apiKey)
sleep 3

# one folder per queue — the folder is the status
for q in Answered Escalated Waiting; do
  curl -s -X POST https://api.mailgi.xyz/v1/mailboxes \
    -H "Authorization: Bearer $KEY" \
    -H "Content-Type: application/json" \
    -d "{\"name\":\"$q\"}"
done

# => {"id":"h","name":"Answered","parentId":null}

The triage loop

Poll the inbox, take what is unseen, fetch each in full, decide, act. Reading is unmetered, so polling costs nothing but the request. A webhook removes the latency if you would rather be told — see SKILL.md §7 — but the loop below is the same either way once you have a message id.

Note the two PATCH calls at the end. Marking seen is what stops the next pass picking the message up again; moving it is what records the outcome. Both return 204 with no body.

INBOX=a   # role "inbox" — from GET /v1/mailboxes

while true; do
  curl -s "https://api.mailgi.xyz/v1/mail?mailboxId=$INBOX&limit=25" \
    -H "Authorization: Bearer $KEY" \
  | jq -r '.messages[] | select(.seen == false) | .id' \
  | while read -r ID; do

      MSG=$(curl -s https://api.mailgi.xyz/v1/mail/$ID \
        -H "Authorization: Bearer $KEY")

      FROM=$(echo "$MSG" | jq -r '.from[0].email')
      SUBJ=$(echo "$MSG" | jq -r '.subject')
      BODY=$(echo "$MSG" | jq -r '
        .bodyValues[.textBody[0].partId].value')

      # your model decides: answer | escalate | wait
      ACTION=$(classify "$SUBJ" "$BODY")

      if [ "$ACTION" = "answer" ]; then
        REPLY=$(draft "$SUBJ" "$BODY")
        curl -s -X POST https://api.mailgi.xyz/v1/mail/send \
          -H "Authorization: Bearer $KEY" \
          -H "Content-Type: application/json" \
          -d "$(jq -n --arg t "$FROM" --arg s "Re: $SUBJ" \
                      --arg b "$REPLY" \
                '{to:[$t],subject:$s,textBody:$b}')"
      fi

      # seen = processed;  folder = outcome
      curl -s -X PATCH https://api.mailgi.xyz/v1/mail/$ID/flags \
        -H "Authorization: Bearer $KEY" \
        -H "Content-Type: application/json" -d '{"seen":true}'

      curl -s -X PATCH https://api.mailgi.xyz/v1/mail/$ID/move \
        -H "Authorization: Bearer $KEY" \
        -H "Content-Type: application/json" \
        -d "{\"mailboxId\":\"$(box_for "$ACTION")\"}"
    done
  sleep 60
done

What this can't do yet

Four real constraints. None is fatal for a support queue, but all four change how you build, and you should know them before you start rather than after.

01

Replies don't thread properly

The send endpoint takes to, cc, bcc, subject, textBody, htmlBody and replyTo — and nothing else. There is no inReplyTo, and passing one is silently ignored: you get a normal 200 and a message id, so it looks like it worked.

In practice you send a new message with Re: on the subject. Gmail and most clients group by subject and participants, so the customer usually sees a normal thread. A strict client will show it separately.

02

Webhooks now exist

POST /v1/webhook-endpoints registers an HTTPS URL and Mailgi POSTs a signed notification as mail arrives — for a support queue the better choice, since you already run a server. One thing to watch: the endpoint is disabled after six consecutive delivery failures and does not recover on its own, so a broken deploy can silence it quietly. There is still no WebSocket.

03

No search

The only filter is mailboxId — which folder. There is no subject, sender or full-text search, and unknown query parameters are ignored rather than rejected, so a search attempt returns the whole inbox and looks like it worked. To find a customer's history, page through and match yourself, or keep your own index as you triage.

04

No attachments

Not supported in either direction today. A customer who sends a screenshot gets their message delivered, but the agent sees only the text. If your support flow depends on receiving files, this is the constraint that should stop you — it is the one with no workaround.

Worth doing from the start

policy

Separate deciding from sending

Classify and draft every message, but only auto-send the classes you have decided are safe. Everything else goes to an Escalated folder, unanswered. One bad automated reply costs more than a slow human one.

honesty

Say it's an agent

A signature line naming the agent and offering a route to a human. Customers forgive a machine that says so and resent one that pretended. It also reduces the chance of a reply being reported as spam.

headroom

Watch the domain ramp

A newly verified domain is capped at 100 sends a day for the whole organisation, rising to 5,000 after thirty days. Launching a support queue on a fresh domain means the cap is lowest exactly when volume is highest.

Questions, answered

01Can an AI agent reply to customer emails in a thread?

It can reply, but Mailgi doesn't set threading headers. The send endpoint accepts to, cc, bcc, subject, textBody, htmlBody and replyTo and nothing else — there's no inReplyTo, and passing one is silently ignored rather than rejected. In practice you send a new message with Re: prefixed, which most clients group by subject and participants, so it looks right to the customer. A strict client will show it as separate.

02How does an agent know a new support email arrived?

Either way, and for a support queue a webhook is the better one. POST /v1/webhook-endpoints registers an HTTPS URL and we POST a signed notification with the sender, subject and a preview as mail arrives — you are already running a server, so the usual objection does not apply. Polling GET /v1/mail every sixty to ninety seconds also works and costs nothing, since reading is unmetered. There is no WebSocket.

03How do you stop an AI agent sending a bad reply?

Separate deciding from sending. Have the agent classify each message and draft a response, then apply a rule about which classes may go out automatically — order status yes, refund request no. Anything outside that set gets filed for a human, unanswered. That's a policy decision in your own code rather than a Mailgi feature, and it's the single most useful safeguard you can add.

04Can several agents share one support inbox?

Not as a shared mailbox, and it's usually the wrong shape anyway. Cleaner is one address per queue — support@, billing@, sales@ each registered as its own agent from the same token, each with its own key and inbox. If you do want several workers on one address, issue multiple API keys to that agent and hand out work in your own code: the API has no locking to stop two workers grabbing the same message.

05What happens to mail that arrives before the domain verifies?

Inbound starts working as soon as the TXT and MX records verify, which is the first half of setup. Outbound stays blocked until DKIM, SPF and DMARC verify separately — so there's a window where the agent can receive and read support mail but not answer it. Publish the full record set before announcing the address, or you'll accumulate a queue you can't reply to.

Ready to attach a domain?

The DNS half takes about ten minutes, and the dashboard verifies each record as it goes live.

Open the dashboard →