Skip to navigation

Server and agent flows

Call the API yourself to run the check from your server or add approval to an agent's tool.

Use the API directly when there’s no browser for the embed, such as a check from your server or an MCP tool that binds a policy. For a website, use the embed, which handles all of this.

Creating the review from your server is also the most secure setup: the quote goes from your server to HumanApproved, so it never passes through the agent.

Before you start

Set your keys and your region’s address as environment variables. Use test keys while you build.

export HUMANAPPROVED_URL="https://uk.humanapproved.com" # your region's address
export HUMANAPPROVED_PUBLISHABLE_KEY="pk_test_…" # one product
export HUMANAPPROVED_SECRET_KEY="sk_test_…" # servers only

Every call except /api/v1/server/* needs an Origin header that is one of the product’s allowed websites for the key’s environment. Browsers send it themselves. From a server, send it yourself, for example Origin: https://www.example.com.

Make sure the check asks for approval

From a server there’s no browser to observe, so HumanApproved only sees your server’s own request. Decide on the product instead:

  • Every purchase through the flow is an agent’s (for example an MCP tool): set the product’s When to ask for approval to Every purchase and publish. Every check returns review.
  • You know which purchases are an agent’s: set the product to use your own signal on its Agent detection page, and send "signal": { "source": "mcp", "isAgent": true } with the check. It returns review unless the product is set to never ask (monitor only).

The lifecycle

1

Create a check

Send your quote reference with the publishable key.

curl -X POST $HUMANAPPROVED_URL/api/v1/checks \
-H "Authorization: Bearer $HUMANAPPROVED_PUBLISHABLE_KEY" \
-H "Origin: https://www.example.com" \
-H "Content-Type: application/json" \
-d '{ "quoteRef": "HTA-4471920", "userId": "CUST-1001" }'

The response is continue or review:

continue
{ "checkId": "chk_…", "outcome": "continue" }
review
{
"checkId": "chk_…",
"outcome": "review",
"review": {
"id": "rv_…",
"token": "…",
"url": "https://uk.humanapproved.com/r/rv_…",
"expiresAt": "2026-11-19T09:14:23.000Z",
"brand": { "name": "Acme Travel" },
"agentPanel": { "text": { "relay": "…", "never": "…" } }
}
}

On continue, go ahead and report the result. On review, keep the review’s id and token on your server against the quote reference. The token is shown once, and only it can send the quote and read the status.

2

Send the quote

Send the quote and the customer’s mobile with the review’s token, once, while the review is waiting for it. The quote must match the product’s published fields; fields it doesn’t define are dropped.

curl -X PUT $HUMANAPPROVED_URL/api/v1/reviews/$REVIEW_ID/quote \
-H "Authorization: Bearer $REVIEW_TOKEN" \
-H "Origin: https://www.example.com" \
-H "Content-Type: application/json" \
-d '{
"quote": {
"policyholder": { "firstName": "Sam" },
"destination": "Japan",
"dates": { "from": "2026-12-03", "to": "2026-12-17" },
"travellers": 2,
"cover": "Comprehensive",
"excess": 200,
"premium": { "amount": 412.6, "currency": "GBP" }
},
"mobile": "+44 7700 900482"
}'

Then show the customer, or the agent, the approval link (url). A missing or wrongly typed field returns 422 invalid_quote, with fields listing each one. A second send returns 409 quote_already_set: treat it as sent.

3

Poll the status

curl $HUMANAPPROVED_URL/api/v1/reviews/$REVIEW_ID/status \
-H "Authorization: Bearer $REVIEW_TOKEN" \
-H "Origin: https://www.example.com"
statusMeans
awaiting_quoteThe quote hasn’t been sent yet.
pendingWaiting for the customer.
approvedThe customer approved. Go ahead.
declinedThe customer declined. Don’t go ahead. declineReason gives the reason, if they chose one.
expiredThe link expired. Start again with a new check.

Poll every 3 seconds, then every 10 seconds after 10 minutes. After an error, slow down to at most once every 60 seconds. Stop at approved, declined, expired or the review’s expiresAt. Before you bind, verify the review on your server with your secret key.

4

Report the result

Tell HumanApproved how the check ended, with the publishable key.

curl -X POST $HUMANAPPROVED_URL/api/v1/checks/$CHECK_ID/result \
-H "Authorization: Bearer $HUMANAPPROVED_PUBLISHABLE_KEY" \
-H "Origin: https://www.example.com" \
-H "Content-Type: application/json" \
-d '{ "result": "completed" }'
resultSend it when
completedYou proceeded after continue, or after the customer approved.
fail_openYou proceeded without a response, for example the check timed out.
abandonedYou didn’t go ahead: declined, expired, or the customer gave up.

The response is 204. Only the first result for a check is kept, so retrying is safe.

An agent can’t approve: only the customer can. The agent passes the link to the customer and checks back later. Give it the review’s url with instructions, keep the review’s ID and token on your server against the quote reference, and check the status when the agent calls again.

Never give the agent the review token, or any of your keys. The token is the review’s credential: keep it on your server. Return only the approval link and instructions.

This example bind_policy tool for an agent, for example in an MCP server, runs on your server:

bind-policy.js
// `call` sends a request as in The lifecycle, and `PK` is your publishable key.
// `reviews`, `bind` and `quoteDetails` are your own.
export async function bindPolicy({ quoteRef }) {
const saved = await reviews.get(quoteRef);
if (saved) {
const { status } = await call(`/api/v1/reviews/${saved.id}/status`, saved.token);
if (status === "approved") return { status: "bound", policy: await bind(quoteRef) };
if (status === "pending") return askCustomer(saved.url, saved.panel);
await reviews.delete(quoteRef); // declined or expired: start again
}
const check = await call("/api/v1/checks", PK, "POST", { quoteRef });
if (check.outcome === "continue") return { status: "bound", policy: await bind(quoteRef) };
const { id, token, agentPanel } = check.review;
const { url } = await call(`/api/v1/reviews/${id}/quote`, token, "PUT", await quoteDetails(quoteRef));
await reviews.set(quoteRef, { id, token, url, panel: agentPanel });
return askCustomer(url, agentPanel);
}
// What the agent gets: the link and instructions, never the token.
const askCustomer = (url, panel) => ({
status: "approval_needed",
approvalUrl: url,
instructions: [panel.text.relay, panel.text.instructions, panel.text.never, url].filter(Boolean).join(" "),
});

reviews, bind and quoteDetails are your own: a store for the review, your bind, and the quote and mobile for that reference from your own records. Report each check’s result, and treat a 401 or 403 on the status call as expired, as described in The lifecycle.

Use the product’s own words

The check’s review.agentPanel.text holds every piece of wording the embed’s pause panel shows: your product’s wording from its Agent popup page, or the standard wording. Use it so an agent gets the same wording whichever way it buys:

  • relay asks the agent to send the link to the customer.
  • instructions holds your own extra instructions, empty when there are none.
  • never tells the agent not to open or approve the link itself.

The text is plain, with no markup or links. Some pieces contain placeholders, such as {expiry}, {reason} and {brand}, for you to fill in. Whatever you show, make sure the agent knows not to open or approve the link itself. See The pause panel.