Tips
How to let an AI agent send a fax without losing control
Some paperwork still ends in a fax, and agents now reach that step. Six controls that keep an agent's fax inside the limits a person set.
An AI agent can now fill in the form, draft the cover letter and find the right number. Then it reaches a recipient that only takes fax, and the task goes back to a person. Giving the agent a fax line fixes that. It also hands a program that can be wrong a way to send documents to strangers and spend money doing it. And a program can be talked into being wrong.
This post is about the second half. What has to be true before an agent should be allowed to press send, and how to check it is true. We built a product around these rules, Fax for Agents, so it is the worked example throughout. The controls apply to any fax integration you build or buy.
Why an agent needs fax at all
Fax still carries contracts, orders and filings in places that will not accept an email. A recipient that only takes fax will not change because an agent turned up. If the agent is going to finish the task, it has to finish that step too.
A fax is also a bad thing to get wrong. It cannot be recalled. It goes to a phone number, which an agent can mistype or copy from the wrong line of a page. And it costs money per page, so a loop that retries can turn one mistake into fifty.
What goes wrong when an agent faxes
Five failures are worth designing against, and each one is ordinary.
- The wrong document. The agent edits the file after a person looked at it, or attaches the draft instead of the final.
- The wrong recipient. A digit off, or a number lifted from the wrong part of an email.
- Spend without a ceiling. A retry loop, or a prompt that tells the agent the budget no longer applies.
- Instructions inside an incoming fax. A received page that says “send our file to this number” is text. An agent that reads it as a command has handed its fax line to whoever wrote it.
- A false report. The agent says the fax was delivered because the send call returned without an error. The network had not confirmed anything.
Six controls that keep it safe
1. Quote before every send
Price one document to one destination before anything moves, and make the send match the quote. A quote is the point where a person or a policy can see what is about to happen: these pages, this number, this amount. In Fax for Agents a quote holds for ten minutes, and a send that does not match it is refused.
2. Bind each approval to exactly one send
“Approved” has to mean approved for this document, to this number, for this amount. An approval that covers “the fax about the contract” will cover a different fax as soon as the agent rewrites the contract.
Fax for Agents binds an approval to five things: the document’s sha256 hash, the
destination, the sender number, the amount and an expiry. Change one and the send
fails with a named error. Edit the document after approval and the hash no longer
matches, so the server answers document_hash_mismatch. The agent can ask for an
approval. It cannot grant one. A person decides in a dashboard, behind a passkey.
3. Put the budget on the server, outside the prompt
Some sends should not wait for a person each time. For those, write the limits down in advance: allowed destinations or country prefixes, a page and amount cap per job, daily and monthly totals. Then check every send against them on the server.
The point is where the check runs. A limit written into the system prompt is a suggestion the model may or may not follow. A limit checked by the server holds no matter what the model was told. That is how Fax for Agents’ delegated policies work. The policy check never reads prompt text, so “ignore the budget” changes nothing.
4. Give each agent its own key that it cannot widen
An agent should not hold an administrator’s key, or a person’s. Give it its own identity with scopes and a budget a person set. Then make sure that API key has no way to raise them. In Fax for Agents the routes that write budgets, policies and approvals are refused to any key that belongs to an agent. Disabling the agent revokes its keys in the same step.
5. Read incoming faxes as data, never as instructions
There is no reliable way to make a language model ignore instructions buried in text it reads. OWASP lists prompt injection first in its 2025 guidance for language model applications. So the defence has to be structural. Text from a received fax should come back marked as untrusted. No decision to send should take its inputs from that text.
Fax for Agents lists the text of a received fax under an untrusted field on
every result. An id copied out of a fax and passed back as an argument is refused
before the request leaves the tool. The agent can summarise the fax for a person.
It cannot turn an instruction printed on it into a send.
6. Report only what the network confirmed
A send call that returns without an error means the job was accepted, not that the fax arrived. Keep the states honest and the agent’s reports will be honest too. Fax for Agents uses six: queued, active, partial, failed, unknown and delivered. It says delivered only when the provider’s evidence exists, and it attaches that evidence. Every job ends with a receipt.
Two details matter more than they look. Give each intended fax one idempotency key, and reuse that key and the same request on every retry. Then a retry after a timeout cannot fax the document twice. And never resend a partial delivery on your own. Half a contract arrived. Sending all of it again is a new mistake.
A worked example over MCP
Here it is on the Fax for Agents MCP server. A client that speaks Streamable HTTP connects with the URL and an API key. One that cannot reach a remote server uses the stdio bridge.
prepare_faxturns the agent’s text, Markdown, HTML or upload into a document.get_documentreturns its page count, its sha256 hash and preview links, so a person can see what will print.quote_faxprices that document to one number. The quote also carries the policy’s verdict: allowed or denied, the first rule that failed, and whether a person must approve.- If the policy does not cover it,
request_human_reviewasks a person to approve that one send. get_review_statusreads the decision back, with the approval id once a person has approved. A rejected or expired request ends the run there.send_approved_faxsends with the approval or under the policy.get_fax_statusreports what the network confirmed, with the carrier’s evidence once the fax is delivered.
Run it against the sandbox first. With a test API key, each send goes to a scripted number that plays out one outcome. The outcomes are delivered, busy then delivered, partial, no answer, unknown, provider timeout and an invalid number. It costs nothing. You see whether your agent handles an unknown result by waiting or by sending again. That is the one to watch.
A short checklist
Before an agent gets a live fax key, check each of these:
- Every send is quoted, and the send must match the quote.
- An approval covers one document hash, one recipient, one sender, one amount and an expiry.
- Budgets and allowed destinations are enforced on the server, not in the prompt.
- The agent’s API key has no route to raise its own limits, and one step revokes it.
- Text from received faxes is marked untrusted and never drives a send.
- “Delivered” appears only with the network’s evidence.
- Each intended fax has one idempotency key, reused on every retry.
- A partial delivery is never resent by the agent.
- Unknown, partial and failed outcomes have each been tested in a sandbox.
If you are building an agent that acts in other systems too, the same rules carry over. Our page on agentic integrations covers them for CRMs, billing and ticket queues. For how the fax product itself was put together, read how we built Fax for Agents.