Skip to main content
IP matching is a security upgrade for the on-ramp widget, and is mandatory for going live. On top of signing your widget URLs, you bind each URL to a hash of the customer’s IP address at the point you generate it. When the widget loads, MoonPay hashes the IP address it observes and compares it to the value signed into the URL. If the observed IP address does not match the hash, the widget shows an error and does not load. This protects against intercepted or stolen URLs being used by anyone other than the intended customer.
IP matching is required to go live with the on-ramp widget. Build and test your integration in sandbox first. MoonPay confirms enforcement is active on your account before you go live.

Requirements

A widget URL passes IP matching when both of the following are true:
  • The URL is signed with your secret key. See URL signing.
  • The URL includes an allowedIpAddress parameter set to a hash of the customer’s IP address, and that hash is part of the signed query string.

Before you begin

  • Get your secret key from the Developers > API Keys page of your MoonPay dashboard. Keep it on your backend and never expose it client-side.
  • Make sure your backend can capture the customer’s live, public IP address at the point of URL generation.

How it works

Generate a signed URL with IP matching

Build the URL on your backend in two steps: hash the customer’s IP address and add it as allowedIpAddress, then sign the full query string. We support both IPv4 and IPv6.

Step 1: Hash the customer’s IP address

Capture the customer’s public IP address, hash it with your secret key using HMAC-SHA256, and append the result to the widget URL as the allowedIpAddress parameter.
Node.js
A mismatch blocks the widget from loading. A customer’s IP address can change between the point you generate the URL and the point they open it, for example on a VPN, a proxy, a corporate network, or a mobile device switching between Wi-Fi and cellular. Generate the signed URL as close as possible to when the customer opens the widget. On iOS, iCloud Private Relay causes a guaranteed mismatch for widgets opened in Safari-based views.
Capture the IP address of the device that will open the widget, on the same request that generates the signed URL. Canonicalize it to a stable string before hashing: HMAC is sensitive to any formatting difference, so the value you hash must exactly match the value MoonPay observes.
IP matching is an exact string comparison of the hash, not a semantic comparison of the address.

Step 2: Sign the URL

Sign the full query string, including allowedIpAddress, with the same secret key. Append the result as the signature parameter.
Node.js
For the full signing flow, including SDK-based signing and PHP examples, see URL signing.

What to expect

When the IP address does not match, the customer sees an “Unverified Connection” error in place of the widget:
Unverified Connection We couldn’t verify this request. If this continues, contact support through the official MoonPay app or website.

iCloud Private Relay

iCloud Private Relay is an iCloud+ feature, on by default for iCloud+ subscribers, that routes Safari traffic through an Apple relay. When a customer with Private Relay opens the widget in a Safari-based view, MoonPay observes an Apple relay egress IP address instead of the customer’s real IP address. Your backend captures the real IP address from your app’s own traffic, so the two never match and the widget fails to load for every Private Relay user. Whether your integration is affected depends on how you open the widget: MoonPay recommends a WebView on iOS and Android, and most integrations that follow the integration design guide are not affected. Some iOS integrations open the widget in SFSafariViewController instead, since it’s a simpler path to Apple Pay support. If that’s your integration, handle Private Relay as described below.

Handle Private Relay sessions

Detect Private Relay before you generate the widget URL, and omit allowedIpAddress from the signed URL when it is active. MoonPay skips IP enforcement for that session but still validates the signature, so the URL cannot be tampered with. In this configuration, you assume responsibility for verifying the customer’s IP address. To detect Private Relay, compare two observations of the customer’s IP address:
  • The IP address your backend observes from your app’s own HTTPS traffic. This traffic bypasses the relay.
  • The IP address your backend observes from a request made inside the Safari view, for example a lightweight endpoint the view loads once and that redirects back to your app with the observed IP address.
If the two match, Private Relay is not active and you include allowedIpAddress as usual. If they differ, omit it:
Node.js
Do not sign the relay IP address instead of omitting allowedIpAddress. Relay egress IP addresses are only stable per destination website, so the relay IP your backend observes can differ from the relay IP MoonPay observes. Canonicalization does not help here either: it normalizes different textual forms of the same address, not two genuinely different addresses.

Dual-stack devices

A device can reach your backend over one IP address family and the widget over the other. This can happen through carrier NAT64/464XLAT, or simply because the device picks a different address family for each separate connection it makes. A real IPv4 address and a real IPv6 address for the same device are two different addresses, not two notations of the same one, so a single allowedIpAddress hash can never match both.

Send both IP addresses

If your backend can capture both address families for the same device, hash each one and send them as a comma-separated list in allowedIpAddress. MoonPay matches against either hash, so the widget loads if the IP address it observes matches either one.
Node.js
You can send just one IP (required) and it still works, but by sending both IPs (optional) you increase the chances of a successful match.
allowedIpAddress accepts at most two comma-separated hashes, one per IP family. If you send more, validation fails and the widget doesn’t load.

Other causes of a mismatch

iCloud Private Relay is the most common cause of a mismatch, but not the only one. Check these before you escalate a mismatch you can’t otherwise explain:
  • Dual-stack IPv4 and IPv6 you can’t capture both sides of. See Dual-stack devices above. If you can only ever capture one address family, confirm you’re not hashing an IPv4 address while the device connects to the widget over IPv6, or the reverse.
  • The IP address changing between signing and opening. Wi-Fi to cellular switches, VPN toggles, corporate networks, and carrier-grade NAT (CGNAT) rotation can all change a customer’s IP address after you generate the signed URL. Generate the URL as close as possible to when the customer opens the widget.
  • VPNs with split tunneling. The same shape as Private Relay: your app’s traffic and the widget’s traffic leave the customer’s device on different addresses.
  • A stale capture. Capturing the IP address at login, at app load, or from a cached session, rather than from the request that generates the signed URL.

Testing

Test your implementation in sandbox before you turn it on in production, using your sandbox API key from the Developers > API Keys page of your MoonPay dashboard. IP matching behaves differently in each environment:

Next steps

  1. Verify both outcomes in sandbox: confirm the widget loads for a matching IP address and returns an error for a mismatch.
  2. If you support iOS, test the Private Relay path. On a device with iCloud Private Relay turned on, confirm your detection step fires and the widget opens without allowedIpAddress. Then turn off Private Relay on the device and confirm the parameter is included again.
  3. Notify your MoonPay integration contact once testing is complete.
  4. MoonPay confirms enforcement is active on your account, and monitors the first days of traffic with you.