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

# Overview

> Receive Platform webhook events and verify that MoonPay sent them.

Register an endpoint in your
[webhook settings](https://dashboard.moonpay.com/developers/#webhooks). Create
one setting for test mode and one setting for live mode. The two settings can
use the same URL.

MoonPay sends a webhook when a customer's KYC status changes. MoonPay also
sends a webhook when a buy transaction changes.

## Requirements

Serve your endpoint over `HTTPS`.

Return a `2xx` status code within 5 seconds. A non-2xx response, a timeout, or
a connection error is a failed delivery. MoonPay then retries the event.

If the handler needs more than 5 seconds, return `200` first. Process the
event after that response.

## Verify the signature

MoonPay sends `Moonpay-Signature-V2` on each request. Verify that header. The
value has a timestamp and a signature, for example
`t=1492774577,s=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd`.

`t` is a Unix timestamp. `s` is a lowercase hex HMAC-SHA256. The signed text is
the timestamp, a period, and the raw request body. The key is your
[webhook API key](https://dashboard.moonpay.com/developers/) from the
Developers page.

MoonPay also sends `Moonpay-Signature`. Verify `Moonpay-Signature-V2`.

> [!WARNING]
>
> Verify the signature on your server. Keep the webhook API key off the client.

Read `Moonpay-Signature-V2` from the request. Compute the digest from the raw
body. Compare `s` to that digest with a constant-time check.

Return `400` when the header is missing. Return `400` when `t` or `s` is
missing. Return `400` when the signature differs.

Keep the raw body for the HMAC. A JSON parser changes the bytes. The digest
then does not match.

```typescript theme={null}
// Server-side, webhook API key
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";

const webhookApiKey = "wk_test_123";
const app = express();

app.post(
  "/webhooks/moonpay",
  // The HMAC covers these raw bytes. A parsed body does not.
  express.raw({ type: "application/json" }),
  (req, res) => {
    const header = req.get("Moonpay-Signature-V2");
    if (!header) {
      res.status(400).end();
      return;
    }

    const timestamp = header
      .split(",")
      .find((part) => part.startsWith("t="))
      ?.slice(2);
    const signature = header
      .split(",")
      .find((part) => part.startsWith("s="))
      ?.slice(2);
    if (!timestamp || !signature) {
      res.status(400).end();
      return;
    }

    const expected = createHmac("sha256", webhookApiKey)
      .update(`${timestamp}.${req.body.toString("utf8")}`)
      .digest("hex");
    const received = Buffer.from(signature);
    const computed = Buffer.from(expected);

    if (
      received.length !== computed.length ||
      !timingSafeEqual(received, computed)
    ) {
      res.status(400).end();
      return;
    }

    // A response after 5 seconds is a failed delivery.
    res.status(200).end();
  },
);
```

## IP addresses

MoonPay sends webhooks from these IPv4 addresses:

* `34.76.118.184`
* `34.77.238.249`
* `34.79.17.44`
* `34.89.242.107`
* `34.140.248.51`
* `34.159.58.44`
* `34.159.89.69`
* `35.246.202.26`

Your network must permit traffic from these addresses. MoonPay does not send
webhooks from IPv6 addresses. Outbound IPv6 support ended on July 15, 2026.

## Delivery and retries

MoonPay retries a failed delivery. MoonPay stops after 10 attempts.

MoonPay can deliver the same event more than once. A repeat delivery uses the
same `webhook-id` header. A repeat delivery must not change the result of the
first delivery.

Events can arrive out of order. Arrival order does not show which payload is
newer.

## Events

Each event is an HTTP `POST`. The body is JSON. The `content-type` header is
`application/json`.

### Customer identity status updated

MoonPay sends `customer.identity_status_updated` when a customer's KYC status
changes. The
[Customer identity status updated](https://dev.moonpay.com/api-reference/platform/webhooks/customer-identity-status-updated.md)
page lists the fields.

The body contains `id`, `type`, `timestamp`, and `data`. `type` is
`customer.identity_status_updated`. `webhook-id` matches `id`. Compare
`timestamp` to choose the newer event.

### Transaction updated

MoonPay sends `transaction_updated` when a buy transaction changes. The
[transaction updated](https://dev.moonpay.com/api-reference/widget/webhooks/transaction-updated.md) page
lists the fields. Compare `updatedAt` on that payload to choose the newer
transaction state.

## Test a webhook

Use [test mode](https://dev.moonpay.com/platform/overview/test-mode.md) before you use live mode.

To receive a webhook on a machine that is not publicly reachable, forward a
local port with [ngrok](https://ngrok.com/). To inspect a payload without your
own server, use [webhook.site](https://webhook.site/).
