Skip to navigation

Handling the outcome

The onResult callback tells your page how each check and review ended, for your own analytics and monitoring.

Pass onResult to protect() and it’s called once for each outcome of a check or review:

HumanApproved.q.push(["protect", {
// …key, quote, before, quoteDetails
onResult: (result) => {
console.info("HumanApproved", result.outcome, result);
},
}]);

The result

type GateResult = {
outcome: "continue" | "review" | "approved" | "declined" | "expired" | "fail_open";
reason?: "timeout" | "network" | "http_401" | "http_403" | "http_4xx" | "http_5xx" | "error";
checkId?: string; // the check, once HumanApproved has answered
reviewId?: string; // the review, for review and what followed it
quoteRef: string; // your quote reference
declineReason?: { code: string; label: string }; // with declined only
monitored?: true; // for a monitor-only product
};

Outcomes

outcomeWhenComes with
continueThe check found no reason to pause. The step proceeded.checkId
reviewThe step is paused and the agent has the approval link.checkId, reviewId
approvedThe customer approved. The step proceeded.checkId, reviewId
declinedThe customer declined. The step stays paused.checkId, reviewId, declineReason when there is one
expiredThe approval link expired before the customer approved.checkId, reviewId
fail_openThe step proceeded without a response.reason, and checkId if the check returned

A review is followed by approved, declined or expired, or by nothing if the agent goes back to the quote.

onResult isn’t called for a step that proceeds without a check: after an approval of the same quote, or once checks have stopped after a second fail-open.

Fail-open reasons

reason comes with fail_open only:

reasonMeans
timeoutNo response in time (2.5 seconds, or your timeoutMs).
networkNo response at all: a blocked host, DNS, offline, or CORS. Check your Content Security Policy and allowed websites.
http_401The check was refused for its key, for example a revoked key.
http_403The check was refused for its website: the page’s origin isn’t on the product’s allowed websites.
http_4xxAnother refusal, for example the product isn’t published.
http_5xxA server error at HumanApproved.
errorAnything else, for example the panel couldn’t be shown or the quote couldn’t be sent.

Decline reasons

declined comes with declineReason when the customer chose one on the approval page.

codelabel, as the customer saw it
not_meI didn’t ask for this
details_wrongSome details are wrong
changed_mindI’ve changed my mind
otherAnother reason
{ "code": "changed_mind", "label": "I've changed my mind" }

Branch on code: it’s stable. declineReason is left out when the customer gave no reason. Review not_me declines: the customer says they didn’t ask for this purchase.

Monitor-only products

A press on a monitor-only product goes straight through, and onResult is called afterwards with monitored: true:

  • continue with the checkId once the check returns, or
  • fail_open with the reason if it fails.

A page that unloads first (for example, a full navigation to your payment page) receives continue as it unloads, without a checkId. Code that only reads outcome sees the same values as for any other product.

What it doesn’t get

onResult gets no personal data: no quote details, no mobile, no userId.

How it’s called

  • After the step continues or pauses, in a microtask, so it doesn’t delay the step.
  • Anything it throws, or a promise it returns that rejects, is caught and logged to the console.
  • If onResult isn’t a function, it’s left out with a console warning, and the step is still protected.

Example: send outcomes to analytics

Google Analytics 4
onResult: (r) => {
window.gtag?.("event", "humanapproved_outcome", {
outcome: r.outcome,
reason: r.reason,
decline_reason: r.declineReason?.code,
monitored: r.monitored === true,
});
},
Datadog Browser Logs
onResult: (r) => {
window.DD_LOGS?.onReady(() => {
window.DD_LOGS.logger.log(
`humanapproved ${r.outcome}`,
{ humanapproved: r },
r.outcome === "fail_open" ? "warn" : "info",
);
});
},

Alert on a rising share of fail_open, especially http_401, http_403 and network: they usually mean a revoked key, a missing allowed website, or a Content Security Policy that blocks the embed.