Skip to content
Last updated

IP Binding

For the complete documentation index, see llms.txt. Append .md to any page URL for its markdown version.

IP binding helps protect your customers' orders. When you create an order, you tell Banxa which network the customer is on. When someone opens the checkout link, Banxa checks that they're on the same network. If they're not, Banxa can block checkout and cancel the order.

IP binding is on for all new partners, so every buy and sell order must include it. Existing partners need it too: Banxa will contact you with a date before turning it on for your account. Until it's on, leaving the fields out changes nothing.

Add IP binding to your integration

1. Get the customer's IP address on your server. Take it from the request the customer's browser or app sends to your server to start the order. If your server sits behind a proxy or load balancer, use the customer's address that it forwards to you, not the proxy's own address. Don't reuse an address saved from an earlier login or session.

Create the order from your server, not from the customer's browser or app: your server is where the customer's IP address and your API secret are.

2. Choose hash or range for each address.

MethodMatchesNeeds your API secretUse it when
hashOnly the exact addressYesYou want the strictest check.
rangeAny address in the same IPv4 /24 or IPv6 /64 networkNoYour customers' addresses change often, for example on mobile networks, or you only have an x-api-key. A range also matches other people on the same network.

You can use a different method for each address.

3. Add one item per IP version. If you know both the customer's IPv4 and IPv6 address, send both. Customers often reach checkout over IPv6 even when your site saw IPv4. If checkout is opened from an IP version you didn't send, Banxa records it but never blocks the order for it.

An IPv4 address written in IPv6 form, such as ::ffff:203.0.113.7, counts as IPv4: send 203.0.113.7 with "family": "v4".

4. Send ipBinding with the order.

"ipBinding": [
  { "method": "hash",  "family": "v4", "value": "31e085138870946bf3e30b6e6709089d14909607b10f1b2138ddc7aaa0d36057" },
  { "method": "range", "family": "v6", "value": "2001:db8:1234:5678::/64" }
]

If the customer uses iCloud Private Relay, checkout sees Apple's relay address instead of the one your server saw. For those customers, send "ipBindingOmitted": "private_relay" instead of ipBinding. Never send both.

You can also send originalUrl, the page the customer was on when they started the order. It's optional.

See the full example request.

5. Handle the results.

  • If IP binding is on for your account and an order has neither ipBinding nor ipBindingOmitted, the order isn't created and you get a 422 with IP_BINDING_MISSING. See Error Codes.
  • If Banxa cancels an order because checkout was opened from a different network, the cancelled order webhook includes "reason_code": "IP_BINDING_MISMATCH". See Webhooks.

Banxa sets, per account, whether a mismatch only gets recorded or blocks checkout. There's nothing for you to configure.

Hashing the IP address

A hash value is the HMAC-SHA256 of the IP address, written as hexadecimal:

value = hex(HMAC-SHA256(key = your API secret, message = the IP address as text))
  • Key: the API secret (HMAC secret) that belongs to the same key pair as the x-api-key you create the order with. Not the x-api-key itself. If you have several key pairs, use the one that matches that x-api-key, which may differ from the pair that signs your webhooks.
  • Message: the IP address in its standard short form. IPv4 as dotted decimal, such as 203.0.113.7. IPv6 in lower case with the longest run of zeros written as :: (RFC 5952), such as 2001:db8::7 rather than 2001:0DB8:0:0:0:0:0:7.
  • Result: 64 hexadecimal characters. Upper or lower case both work.

Keep the key pair until the order is paid: don't delete or revoke it while an order is still unpaid. Banxa checks the IP address every time the customer opens checkout before paying, and it can't check a hash without the key pair.

Use these values to test your code. With the secret example-api-secret:

Customer's IP addressSend asvalue
203.0.113.7"family": "v4"31e085138870946bf3e30b6e6709089d14909607b10f1b2138ddc7aaa0d36057
2001:0DB8:0000:0000:0000:0000:0000:0007"family": "v6", hashed as 2001:db8::7cee45e0bd6238d4c3daf71fcb7b7f451a3ef50a4ff1f6ab2d31fcd424efdb73b

Code examples:

import hmac, hashlib, ipaddress

def ip_binding_hash(ip: str, api_secret: str) -> dict:
    address = ipaddress.ip_address(ip)
    # An IPv4 address written in IPv6 form is sent as IPv4.
    if address.version == 6 and address.ipv4_mapped:
        address = address.ipv4_mapped
    # str() gives the standard short form, including RFC 5952 for IPv6.
    value = hmac.new(api_secret.encode(), str(address).encode(), hashlib.sha256).hexdigest()
    return {"method": "hash", "family": f"v{address.version}", "value": value}

Building a range

A range value is the customer's network in CIDR form, with every bit after the prefix set to zero:

  • IPv4: the /24 network. For 203.0.113.7, send 203.0.113.0/24.
  • IPv6: the /64 network. For 2001:db8:1234:5678::7, send 2001:db8:1234:5678::/64.

Banxa rejects any other prefix length, and a value like 203.0.113.7/24 where the host part isn't zero.

The Node.js, PHP, Java and Swift examples use normaliseIp, parseIp and formatIp from the hashing examples above.

import ipaddress

def ip_binding_range(ip: str) -> dict:
    address = ipaddress.ip_address(ip)
    if address.version == 6 and address.ipv4_mapped:
        address = address.ipv4_mapped
    prefix = 24 if address.version == 4 else 64
    network = ipaddress.ip_network(f"{address}/{prefix}", strict=False)
    return {"method": "range", "family": f"v{address.version}", "value": str(network)}