> This page is for version 🇬🇧 UK.
> For other versions, use one of these documentation indexes:
> - 🇦🇺 AU (default): https://docs.humanapproved.com/au/llms.txt
> - 🇬🇧 UK: https://docs.humanapproved.com/uk/llms.txt
> - 🇺🇸 US: https://docs.humanapproved.com/us/llms.txt

> This is HumanApproved's documentation, as Markdown for AI agents. /llms.txt lists every page; /llms-full.txt has them all. If you're helping someone integrate HumanApproved, start with /guides/ai-coding-agents.

# Verify on your server

> How to check a HumanApproved review from your server with a secret key before binding an insurance policy.

## How it works

#### Keep the review ID with the quote

When a review starts, you get its ID (`rv_…`). With the embed, `onResult` passes it to your page: send it to your server and store it against the quote.

**`checkout-approvals.js`**

```js title="checkout-approvals.js"
HumanApproved.q.push(["protect", {
  // …key, quote, before, quoteDetails
  onResult: (r) => {
    if (r.outcome === "approved") {
      fetch("/api/quotes/HTA-4471920/approval", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ reviewId: r.reviewId }),
      });
    }
  },
}]);
```

You can also send the ID with the payment form, for example in a hidden field.

In a [server or agent flow](/uk/guides/server-and-agent-flows), your server already has the ID from the check's response.

#### Read the review at bind

Call the server endpoint with your secret key. It's for servers only: it sends no CORS headers and needs no `Origin`. Set your secret key and your region's address as environment variables:

```bash
export HUMANAPPROVED_URL="https://uk.humanapproved.com"  # your region's address
export HUMANAPPROVED_SECRET_KEY="sk_test_…"              # servers only
```

**`curl`**

```bash title="curl"
curl $HUMANAPPROVED_URL/api/v1/server/reviews/$REVIEW_ID \
  -H "Authorization: Bearer $HUMANAPPROVED_SECRET_KEY"
```

**`Node`**

```js title="Node"
const res = await fetch(
  `${process.env.HUMANAPPROVED_URL}/api/v1/server/reviews/${process.env.REVIEW_ID}`,
  { headers: { authorization: `Bearer ${process.env.HUMANAPPROVED_SECRET_KEY}` } },
);
const review = await res.json();
```

**`200 OK`**

```json title="200 OK"
{
  "id": "rv_01jcz3fa1q8w3n5r7t9v1x3z5b",
  "status": "approved",
  "quoteRef": "HTA-4471920",
  "userId": "CUST-1001",
  "product": "acme-travel",
  "productVersion": 11,
  "env": "live",
  "expiresAt": "2026-11-19T09:14:23.000Z",
  "approvedAt": "2026-11-18T09:20:41.000Z",
  "declinedAt": null,
  "declineReason": null
}
```

A review from another account or environment returns `404 review_not_found`: a test secret key only reads test reviews.

#### Check it before you bind

Bind only when every check passes:

| Field            | Check                                                                                                  |
| ---------------- | ------------------------------------------------------------------------------------------------------ |
| `status`         | Is `approved`. Anything else (`awaiting_quote`, `pending`, `declined`, `expired`) means don't bind.    |
| `quoteRef`       | Matches the quote you're about to bind. This stops an approval for one quote being reused for another. |
| `env`            | Is `live` in production. A test review is never a real approval.                                       |
| `productVersion` | Is the published version you expect. It's the version whose wording the customer saw.                  |

A purchase with no review ID is one the check let continue, or one that proceeded without an answer. This is expected for most customers when the product asks only when an agent is detected. Where every purchase must be approved, such as an agent flow or a product set to **Every purchase**, refuse to bind without an approved review.

**`Node`**

```js title="Node"
async function assertApproved(reviewId, quoteRef, productVersion) {
  const res = await fetch(`${process.env.HUMANAPPROVED_URL}/api/v1/server/reviews/${reviewId}`, {
    headers: { authorization: `Bearer ${process.env.HUMANAPPROVED_SECRET_KEY}` },
  });
  if (!res.ok) throw new Error(`Review check failed: ${res.status}`);
  const review = await res.json();

  if (review.status !== "approved") throw new Error(`Review is ${review.status}`);
  if (review.quoteRef !== quoteRef) throw new Error("Review is for another quote");
  if (review.env !== "live") throw new Error("Review is not live");
  if (review.productVersion !== productVersion) throw new Error("Review is for another version of the product");
  return review;
}

// At bind:
const review = await assertApproved(process.env.REVIEW_ID, "HTA-4471920", 11); // 11: the product version you published
// …bind the policy you hold on your server for HTA-4471920
```

> **Note**
>
> This check confirms the customer approved the quote with that reference. It doesn't compare the quote's contents. With the embed, the quote the customer sees comes from `quoteDetails` in the browser, so build it from the quote your server holds, and issue a new quote reference whenever the details change. For the strongest assurance, create the review from your server so the quote never passes through the browser: see [Server and agent flows](/uk/guides/server-and-agent-flows).

## Related

* [Server and agent flows](/uk/guides/server-and-agent-flows): create the check from your server, so the quote never passes through the browser.
* [Keys and environments](/uk/concepts/keys-and-environments): publishable keys, secret keys and review tokens.