> ## Documentation Index
> Fetch the complete documentation index at: https://docs.grogov.us/llms.txt
> Use this file to discover all available pages before exploring further.

# Results and signatures

> Find out how a hosted checkout ended, and verify that the result came from GroGov.

A hosted checkout reports its result through the payer's browser. GroGov redirects the payer to your `return_url` or `cancel_url` and adds the result as query parameters, signed with the portal's signing secret.

## Paid

When the payer presses **Done** on the receipt, they go to your `return_url` with:

| Parameter | Value |
| - | - |
| `intent_id` | GroGov's identifier for this checkout |
| `reference_id` | Your `reference_id` |
| `transaction_id` | The payment's identifier in GroGov |
| `status` | `paid` |
| `amount_cents` | The amount charged, in cents, **including the service fee** |
| `sig` | The signature |

```text theme={null}
https://partner.example.com/checkout/return?intent_id=7c1e2d3f-4a5b-4c6d-8e9f-0a1b2c3d4e5f&reference_id=INV-10293&transaction_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890&status=paid&amount_cents=10300&sig=26dcd5…
```

Here the items came to $100.00 and the service fee was $3.00, so `amount_cents` is `10300`.

## Cancelled

When the payer cancels on the checkout page, they go to your `cancel_url` with `intent_id`, `reference_id`, `status=cancelled`, and `sig`. The checkout can no longer be paid. If the payer reopens its link, they see an empty checkout with no line items; start a new checkout if they want to pay.

## Failed to start

When the request to start a checkout fails, no checkout exists. The payer goes to your `cancel_url` with `error=validation_error`, `detail=<code>`, and `sig`, or to the GroGov error page. See [Errors](/guides/hosted-checkout/start-a-checkout#errors) for the codes.

## What is not reported today

<Warning>
  * **You hear about a payment only if the payer presses Done.** If they close the receipt page instead, nothing reaches your `return_url`, even though the payment succeeded.
  * **Nothing is sent when a checkout expires or a card is declined.** A declined payer can try again on the same page; an expired link simply stops working.
  * **`amount_cents` includes the service fee.** Compare it with your item total plus the fee, not with the item total alone.
</Warning>

Treat a missing result as unknown, not as unpaid. Do not ask the payer to pay again only because no result arrived. Contact GroGov support to confirm a payment's status.

## Verifying the signature

Verify `sig` before you act on a result. Anyone can type a URL with `status=paid` in it, but only GroGov has the portal's signing secret. The signature is a hex HMAC-SHA256, keyed with the portal's signing secret, of the result's parameters:

1. Take the parameters GroGov added, except `sig`. Your own query parameters on the address are not signed. GroGov's parameters are `intent_id`, `reference_id`, `transaction_id`, `status`, `amount_cents`, `error`, and `detail`, whichever are present. Do not use these names in your own `return_url` or `cancel_url`.
2. Sort them by name.
3. Join them as `name=value` pairs with `&`, using the decoded values.
4. Compute the HMAC-SHA256 of that text with the signing secret, as lowercase hex.
5. Compare it with `sig` using a constant-time comparison.

A result without a `sig`, or with one that does not match, is unverified. Results carry no timestamp yet, so you cannot reject an old result by its age. Record each `intent_id` you have processed and ignore repeats.

### Worked example

With this signing secret (an example, not a real one):

```text theme={null}
9f2c4e7a1b3d5f6082a4c6e8f0b1d3e5a7c9e1f3b5d7f9a1c3e5f7092b4d6e8a
```

and a payer returning to `https://partner.example.com/checkout/return?order=42`, the browser arrives at:

```text theme={null}
https://partner.example.com/checkout/return?order=42&intent_id=7c1e2d3f-4a5b-4c6d-8e9f-0a1b2c3d4e5f&reference_id=INV-10293&transaction_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890&status=paid&amount_cents=10300&sig=26dcd59a32e0af4e1f4b6dbfed67927d29c4f4fc534e54825a7e0fed03763fbe
```

`order` is yours, so it is left out. The other parameters, sorted and joined, are the text to sign:

```text theme={null}
amount_cents=10300&intent_id=7c1e2d3f-4a5b-4c6d-8e9f-0a1b2c3d4e5f&reference_id=INV-10293&status=paid&transaction_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890
```

Its HMAC-SHA256 with the secret is `26dcd59a32e0af4e1f4b6dbfed67927d29c4f4fc534e54825a7e0fed03763fbe`, which matches `sig`.

<CodeGroup>
  ```ts Node.js theme={null}
  import crypto from "node:crypto";

  const SIGNED = ["intent_id", "reference_id", "transaction_id", "status", "amount_cents", "error", "detail"];

  export function verifyHostedCheckoutResult(url: string, secret: string): boolean {
    const params = new URL(url).searchParams;
    const sig = params.get("sig");
    if (!sig) return false;

    const text = SIGNED.filter((name) => params.has(name))
      .sort()
      .map((name) => `${name}=${params.get(name)}`)
      .join("&");
    const expected = crypto.createHmac("sha256", secret).update(text).digest("hex");

    return sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  }
  ```

  ```python Python theme={null}
  import hashlib
  import hmac
  from urllib.parse import parse_qsl, urlsplit

  SIGNED = {"intent_id", "reference_id", "transaction_id", "status", "amount_cents", "error", "detail"}

  def verify_hosted_checkout_result(url: str, secret: str) -> bool:
      params = dict(parse_qsl(urlsplit(url).query))
      sig = params.get("sig")
      if not sig:
          return False

      text = "&".join(f"{name}={params[name]}" for name in sorted(SIGNED & params.keys()))
      expected = hmac.new(secret.encode(), text.encode(), hashlib.sha256).hexdigest()
      return hmac.compare_digest(sig, expected)
  ```
</CodeGroup>

On your return page, pass the full URL the browser requested, including the query string. Once the signature checks out, match `reference_id` to your record and check `amount_cents` before you mark it paid.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.