{"templateId":"markdown","sharedDataIds":{"sidebar":"sidebar-products/hosted-checkout/sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":["code-group"]},"type":"markdown"},"seo":{"title":"IP Binding: Send the Customer's IP Address | Banxa Hosted Checkout","siteUrl":"https://docs.banxa.com","description":"Send the customer's IP address with POST /v2/buy and POST /v2/sell so Banxa can check checkout is opened from the same network. Steps, hashing, ranges and errors.","image":"https://docs.banxa.com/images/social/docs.png","meta":[{"name":"twitter:card","content":"summary_large_image"}],"llmstxt":{"title":"Banxa Developer Documentation","description":"Integrate crypto-fiat exchange with Banxa's licensed infrastructure: payments, KYC, compliance, and settlement handled. 150+ countries, 45 global licences.","details":{"content":"Two integration products, chosen by how much of the payment experience the partner owns: **Banxa Native API** for payments that happen inside the partner's product (headless bank transfers, HMAC server-to-server; card, Apple Pay and Google Pay on a native sheet via SDK, no Banxa-hosted screens); **Banxa Hosted Checkout** for a Banxa-run payment flow with the broadest payment method coverage (three paths: Referral URL, API, or SDK). Either product can run KYC on either side. Both use the same sandbox and production environments at `https://api.banxa-sandbox.com` and `https://api.banxa.com`.\n\n## When to use Banxa\n\nReach for Banxa when a product needs to let people **buy crypto with fiat or sell crypto for fiat** without the partner holding money-transmitter or VASP licences. Banxa is the licensed counterparty: it takes the payment, runs compliance, and delivers the asset. Typical fits are wallets, exchanges, dApps, gaming platforms and fintech apps adding an on-ramp or off-ramp.\n\nPick the product by how much of the payment experience the partner wants to own. KYC is configurable on both and does not decide it.\n\n- **Banxa Native API** - the payment experience lives inside the partner's product. Bank transfers run completely headless through the API; card, Apple Pay and Google Pay run on a native payment sheet through a Banxa SDK, with no redirect or webview. Needs a backend. Days to weeks to integrate.\n- **Banxa Hosted Checkout** - Banxa runs the payment flow, reached by redirect, embedded iFrame, or SDK. Broadest payment method coverage. No backend required for the referral path. Hours to days to integrate.\n- **Alternative payment methods** (PayPal, iDEAL, Klarna, PIX, VietQR and local methods) are not supported headless. Native integrations offer them by creating those orders through Hosted Checkout, so most Native integrations use both products.\n- **Legacy API** - existing integrations only. Do not start here.\n\nBanxa is not the right tool for crypto-to-crypto swaps, custody or wallet infrastructure, or for any flow where the partner wants to settle funds itself.\n\n## Constraints\n\n- **Authentication**: HMAC credentials must be stored server-side only, never expose in frontend, mobile, or client-side code. HMAC is required for all Native API calls and for KYC sharing in Hosted Checkout; `x-api-key` is used for all other Hosted Checkout endpoints.\n- **`externalCustomerId`**: Required on every buy and sell order. Use a stable opaque identifier, never PII.\n- **`identityReference`**: Must remain constant for the same user across all requests. Must not contain PII. If you attempt to create an identity for an email that already exists, you will receive a 422 / code 81, so retrieve the existing record rather than retrying creation. Use `GET /eapi/v0/identities/{identityReference}?email=user@example.com` to look up the real `identityReference` linked to an email.\n- **`quoteId`**: Only supported by `POST /eapi/v0/ramps` (bank transfer). The React Native SDK and Embedded Payment Button do not accept a `quoteId`, so use indicative pricing (`GET /eapi/v0/price`) for SDK and Embedded Payment Button flows.\n- **Quotes**: Indicative prices are not rate-locked, so refresh close to order creation to minimise rate drift. Locked quotes (`GET /eapi/v0/quote`) expire after approximately 3 minutes and are only valid for bank transfer ramp creation.\n- **Eligibility gate**: Never create a ramp or invoke the SDK when `paymentReady` is `false`. Always check eligibility and satisfy all requirements before payment execution.\n- **Webhooks**: Verify all inbound webhook signatures with HMAC-SHA256 before processing. Return HTTP 200 immediately and process asynchronously.\n- **Product selection**: Choose by payment experience, not by KYC — both products support partner-run or Banxa-run KYC. Banxa Native is for payments rendered inside the partner's product; Banxa Hosted Checkout is for a Banxa-run payment flow. They are separate surfaces with separate credentials and base paths, so never substitute one product's endpoint for the other's. Combining them is expected in one case only: alternative payment methods are not supported headless, so a Native integration creates those specific orders through Hosted Checkout.\n- **SDK scope**: The React Native SDK has no `banxa.customerIdentity` module. Identity and KYC are handled through the Native API only. `primerCallbacks` and `primerSettings` are Native context only and must not be referenced in Hosted Checkout integrations.\n- **Payment method naming**: Never use \"eAPI\" or \"EAPI\" as a product name. The correct name is \"Banxa Native API\". The URL path prefix `/eapi/` is correct and should not be changed.\n"}}},"dynamicMarkdocComponents":[],"compilationErrors":[],"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":1,"id":"ip-binding","__idx":0},"children":["IP Binding"]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["For the complete documentation index, see ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://docs.banxa.com/llms.txt"},"children":["llms.txt"]},". Append ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".md"]}," to any page URL for its markdown version."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["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."]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["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."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"add-ip-binding-to-your-integration","__idx":1},"children":["Add IP binding to your integration"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["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."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["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."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["2. Choose hash or range for each address."]}]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Method"},"children":["Method"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Matches"},"children":["Matches"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Needs your API secret"},"children":["Needs your API secret"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Use it when"},"children":["Use it when"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["hash"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Only the exact address"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Yes"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["You want the strictest check."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["range"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Any address in the same IPv4 ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/24"]}," or IPv6 ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/64"]}," network"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["No"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Your customers' addresses change often, for example on mobile networks, or you only have an ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["x-api-key"]},". A range also matches other people on the same network."]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You can use a different method for each address."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["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."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["An IPv4 address written in IPv6 form, such as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["::ffff:203.0.113.7"]},", counts as IPv4: send ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["203.0.113.7"]}," with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["\"family\": \"v4\""]},"."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["4. Send ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ipBinding"]}," with the order."]}]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"\"ipBinding\": [\n  { \"method\": \"hash\",  \"family\": \"v4\", \"value\": \"31e085138870946bf3e30b6e6709089d14909607b10f1b2138ddc7aaa0d36057\" },\n  { \"method\": \"range\", \"family\": \"v6\", \"value\": \"2001:db8:1234:5678::/64\" }\n]\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If the customer uses iCloud Private Relay, checkout sees Apple's relay address instead of the one your server saw. For those customers, send ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["\"ipBindingOmitted\": \"private_relay\""]}," instead of ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ipBinding"]},". Never send both."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["You can also send ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["originalUrl"]},", the page the customer was on when they started the order. It's optional."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["See the ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/hosted-checkout/docs/api-integration/create-buy-order#with-ip-binding"},"children":["full example request"]},"."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["5. Handle the results."]}]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["If IP binding is on for your account and an order has neither ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ipBinding"]}," nor ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ipBindingOmitted"]},", the order isn't created and you get a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["422"]}," with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["IP_BINDING_MISSING"]},". See ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/hosted-checkout/docs/reference/error-codes#ip-binding"},"children":["Error Codes"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["If Banxa cancels an order because checkout was opened from a different network, the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["cancelled"]}," order webhook includes ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["\"reason_code\": \"IP_BINDING_MISMATCH\""]},". See ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/hosted-checkout/docs/transaction-lifecycle/webhooks#ip-binding-cancellations"},"children":["Webhooks"]},"."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Banxa sets, per account, whether a mismatch only gets recorded or blocks checkout. There's nothing for you to configure."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"hashing-the-ip-address","__idx":2},"children":["Hashing the IP address"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["A hash ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["value"]}," is the HMAC-SHA256 of the IP address, written as hexadecimal:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"value = hex(HMAC-SHA256(key = your API secret, message = the IP address as text))\n"},"children":[]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Key:"]}," the API secret (HMAC secret) that belongs to the same key pair as the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["x-api-key"]}," you create the order with. Not the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["x-api-key"]}," itself. If you have several key pairs, use the one that matches that ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["x-api-key"]},", which may differ from the pair that signs your webhooks."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Message:"]}," the IP address in its standard short form. IPv4 as dotted decimal, such as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["203.0.113.7"]},". IPv6 in lower case with the longest run of zeros written as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["::"]}," (",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://www.rfc-editor.org/rfc/rfc5952"},"children":["RFC 5952"]},"), such as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["2001:db8::7"]}," rather than ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["2001:0DB8:0:0:0:0:0:7"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Result:"]}," 64 hexadecimal characters. Upper or lower case both work."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["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."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Use these values to test your code. With the secret ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["example-api-secret"]},":"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Customer's IP address"},"children":["Customer's IP address"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Send as"},"children":["Send as"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"value"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["value"]}]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["203.0.113.7"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["\"family\": \"v4\""]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["31e085138870946bf3e30b6e6709089d14909607b10f1b2138ddc7aaa0d36057"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["2001:0DB8:0000:0000:0000:0000:0000:0007"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["\"family\": \"v6\""]},", hashed as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["2001:db8::7"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["cee45e0bd6238d4c3daf71fcb7b7f451a3ef50a4ff1f6ab2d31fcd424efdb73b"]}]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Code examples:"]},{"$$mdtype":"Tag","name":"CodeGroup","attributes":{"mode":"tabs"},"children":[{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"python","data-title":"Python","header":{"title":"Python","controls":{"copy":{}}},"source":"import hmac, hashlib, ipaddress\n\ndef ip_binding_hash(ip: str, api_secret: str) -> dict:\n    address = ipaddress.ip_address(ip)\n    # An IPv4 address written in IPv6 form is sent as IPv4.\n    if address.version == 6 and address.ipv4_mapped:\n        address = address.ipv4_mapped\n    # str() gives the standard short form, including RFC 5952 for IPv6.\n    value = hmac.new(api_secret.encode(), str(address).encode(), hashlib.sha256).hexdigest()\n    return {\"method\": \"hash\", \"family\": f\"v{address.version}\", \"value\": value}\n","lang":"python"},"children":[]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"javascript","data-title":"Node.js","header":{"title":"Node.js","controls":{"copy":{}}},"source":"const crypto = require('crypto');\nconst net = require('net');\n\n// Returns the IP address in its standard short form, with its family.\nfunction normaliseIp(ip) {\n    if (net.isIPv4(ip)) return { family: 'v4', address: ip };\n    if (!net.isIPv6(ip)) throw new Error(`Invalid IP address: ${ip}`);\n    // The URL parser writes IPv6 in lower case with the longest run of zeros as :: (RFC 5952).\n    const address = new URL(`http://[${ip}]`).hostname.slice(1, -1);\n    // An IPv4 address written in IPv6 form is sent as IPv4.\n    const mapped = address.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);\n    if (mapped) {\n        const high = parseInt(mapped[1], 16), low = parseInt(mapped[2], 16);\n        return { family: 'v4', address: [high >> 8, high & 255, low >> 8, low & 255].join('.') };\n    }\n    return { family: 'v6', address };\n}\n\nfunction ipBindingHash(ip, apiSecret) {\n    const { family, address } = normaliseIp(ip);\n    const value = crypto.createHmac('sha256', apiSecret).update(address).digest('hex');\n    return { method: 'hash', family, value };\n}\n","lang":"javascript"},"children":[]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"php","data-title":"PHP","header":{"title":"PHP","controls":{"copy":{}}},"source":"// Returns the IP address as bytes: 4 for IPv4, 16 for IPv6.\nfunction parseIp(string $ip): string {\n    if (filter_var($ip, FILTER_VALIDATE_IP) === false) {\n        throw new InvalidArgumentException(\"Invalid IP address: $ip\");\n    }\n    $bytes = inet_pton($ip);\n    // An IPv4 address written in IPv6 form is sent as IPv4.\n    if (strlen($bytes) === 16 && substr($bytes, 0, 12) === str_repeat(\"\\0\", 10) . \"\\xff\\xff\") {\n        $bytes = substr($bytes, 12);\n    }\n    return $bytes;\n}\n\n// Returns the standard short form: dotted decimal for IPv4, RFC 5952 for IPv6.\nfunction formatIp(string $bytes): string {\n    if (strlen($bytes) === 4) {\n        return implode('.', unpack('C4', $bytes));\n    }\n    $groups = array_map('dechex', array_values(unpack('n8', $bytes)));\n    // Find the longest run of two or more zero groups (the first one if tied).\n    $bestStart = -1;\n    $bestLength = 1;\n    for ($i = 0; $i < 8; $i = max($j, $i + 1)) {\n        for ($j = $i; $j < 8 && $groups[$j] === '0'; $j++);\n        if ($j - $i > $bestLength) {\n            $bestStart = $i;\n            $bestLength = $j - $i;\n        }\n    }\n    if ($bestStart < 0) {\n        return implode(':', $groups);\n    }\n    return implode(':', array_slice($groups, 0, $bestStart)) . '::'\n        . implode(':', array_slice($groups, $bestStart + $bestLength));\n}\n\nfunction ipBindingHash(string $ip, string $apiSecret): array {\n    $bytes = parseIp($ip);\n    $value = hash_hmac('sha256', formatIp($bytes), $apiSecret);\n    return ['method' => 'hash', 'family' => strlen($bytes) === 4 ? 'v4' : 'v6', 'value' => $value];\n}\n","lang":"php"},"children":[]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"java","data-title":"Java","header":{"title":"Java","controls":{"copy":{}}},"source":"import javax.crypto.Mac;\nimport javax.crypto.spec.SecretKeySpec;\nimport java.net.InetAddress;\nimport java.nio.charset.StandardCharsets;\nimport java.util.Arrays;\nimport java.util.HexFormat;\nimport java.util.Map;\n\nprivate static final String IPV4 = \"((25[0-5]|2[0-4]\\\\d|1\\\\d\\\\d|[1-9]?\\\\d)\\\\.){3}(25[0-5]|2[0-4]\\\\d|1\\\\d\\\\d|[1-9]?\\\\d)\";\nprivate static final String IPV6 = \"[0-9A-Fa-f:][0-9A-Fa-f:.]*:[0-9A-Fa-f:.]*\";\n\n// Returns the IP address as bytes: 4 for IPv4, 16 for IPv6.\n// An IPv4 address written in IPv6 form comes back as IPv4.\npublic static byte[] parseIp(String ip) throws Exception {\n    // Only pass literals to InetAddress, so it never does a DNS lookup.\n    if (!ip.matches(IPV4) && !ip.matches(IPV6)) {\n        throw new IllegalArgumentException(\"Invalid IP address: \" + ip);\n    }\n    return InetAddress.getByName(ip).getAddress();\n}\n\n// Returns the standard short form: dotted decimal for IPv4, RFC 5952 for IPv6.\npublic static String formatIp(byte[] bytes) {\n    if (bytes.length == 4) {\n        return (bytes[0] & 0xff) + \".\" + (bytes[1] & 0xff) + \".\" + (bytes[2] & 0xff) + \".\" + (bytes[3] & 0xff);\n    }\n    String[] groups = new String[8];\n    for (int i = 0; i < 8; i++) {\n        groups[i] = Integer.toHexString(((bytes[2 * i] & 0xff) << 8) | (bytes[2 * i + 1] & 0xff));\n    }\n    // Find the longest run of two or more zero groups (the first one if tied).\n    int bestStart = -1, bestLength = 1;\n    for (int i = 0, j; i < 8; i = Math.max(j, i + 1)) {\n        for (j = i; j < 8 && groups[j].equals(\"0\"); j++);\n        if (j - i > bestLength) {\n            bestStart = i;\n            bestLength = j - i;\n        }\n    }\n    if (bestStart < 0) {\n        return String.join(\":\", groups);\n    }\n    return String.join(\":\", Arrays.copyOfRange(groups, 0, bestStart)) + \"::\"\n        + String.join(\":\", Arrays.copyOfRange(groups, bestStart + bestLength, 8));\n}\n\npublic static Map<String, String> ipBindingHash(String ip, String apiSecret) throws Exception {\n    byte[] bytes = parseIp(ip);\n    Mac mac = Mac.getInstance(\"HmacSHA256\");\n    mac.init(new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), \"HmacSHA256\"));\n    String value = HexFormat.of().formatHex(mac.doFinal(formatIp(bytes).getBytes(StandardCharsets.UTF_8)));\n    return Map.of(\"method\", \"hash\", \"family\", bytes.length == 4 ? \"v4\" : \"v6\", \"value\", value);\n}\n","lang":"java"},"children":[]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"swift","data-title":"Swift","header":{"title":"Swift","controls":{"copy":{}}},"source":"import CryptoKit\nimport Foundation\n\nstruct InvalidIPAddress: Error {\n    let ip: String\n}\n\n// Returns the IP address as bytes: 4 for IPv4, 16 for IPv6.\nfunc parseIp(_ ip: String) throws -> [UInt8] {\n    var v4 = in_addr()\n    if inet_pton(AF_INET, ip, &v4) == 1 {\n        return withUnsafeBytes(of: &v4) { Array($0) }\n    }\n    var v6 = in6_addr()\n    guard inet_pton(AF_INET6, ip, &v6) == 1 else { throw InvalidIPAddress(ip: ip) }\n    let bytes = withUnsafeBytes(of: &v6) { Array($0) }\n    // An IPv4 address written in IPv6 form is sent as IPv4.\n    if bytes[0..<10].allSatisfy({ $0 == 0 }) && bytes[10] == 0xff && bytes[11] == 0xff {\n        return Array(bytes[12...])\n    }\n    return bytes\n}\n\n// Returns the standard short form: dotted decimal for IPv4, RFC 5952 for IPv6.\nfunc formatIp(_ bytes: [UInt8]) -> String {\n    if bytes.count == 4 {\n        return bytes.map { String($0) }.joined(separator: \".\")\n    }\n    let groups = (0..<8).map { String(UInt16(bytes[2 * $0]) << 8 | UInt16(bytes[2 * $0 + 1]), radix: 16) }\n    // Find the longest run of two or more zero groups (the first one if tied).\n    var bestStart = -1, bestLength = 1, i = 0\n    while i < 8 {\n        var j = i\n        while j < 8 && groups[j] == \"0\" { j += 1 }\n        if j - i > bestLength {\n            bestStart = i\n            bestLength = j - i\n        }\n        i = max(j, i + 1)\n    }\n    if bestStart < 0 {\n        return groups.joined(separator: \":\")\n    }\n    return groups[..<bestStart].joined(separator: \":\") + \"::\"\n        + groups[(bestStart + bestLength)...].joined(separator: \":\")\n}\n\nfunc ipBindingHash(ip: String, apiSecret: String) throws -> [String: String] {\n    let bytes = try parseIp(ip)\n    let key = SymmetricKey(data: Data(apiSecret.utf8))\n    let value = HMAC<SHA256>.authenticationCode(for: Data(formatIp(bytes).utf8), using: key)\n        .map { String(format: \"%02x\", $0) }.joined()\n    return [\"method\": \"hash\", \"family\": bytes.count == 4 ? \"v4\" : \"v6\", \"value\": value]\n}\n","lang":"swift"},"children":[]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"ruby","data-title":"Ruby","header":{"title":"Ruby","controls":{"copy":{}}},"source":"require 'ipaddr'\nrequire 'openssl'\n\ndef ip_binding_hash(ip, api_secret)\n  raise ArgumentError, \"Invalid IP address: #{ip}\" if ip.include?('/')\n  address = IPAddr.new(ip)\n  # An IPv4 address written in IPv6 form is sent as IPv4.\n  address = address.native if address.ipv4_mapped?\n  # to_s gives the standard short form, including RFC 5952 for IPv6.\n  value = OpenSSL::HMAC.hexdigest('sha256', api_secret, address.to_s)\n  { method: 'hash', family: address.ipv4? ? 'v4' : 'v6', value: value }\nend\n","lang":"ruby"},"children":[]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"building-a-range","__idx":3},"children":["Building a range"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["A range ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["value"]}," is the customer's network in CIDR form, with every bit after the prefix set to zero:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["IPv4: the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/24"]}," network. For ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["203.0.113.7"]},", send ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["203.0.113.0/24"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["IPv6: the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/64"]}," network. For ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["2001:db8:1234:5678::7"]},", send ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["2001:db8:1234:5678::/64"]},"."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Banxa rejects any other prefix length, and a value like ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["203.0.113.7/24"]}," where the host part isn't zero."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The Node.js, PHP, Java and Swift examples use ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["normaliseIp"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["parseIp"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["formatIp"]}," from the hashing examples above."]},{"$$mdtype":"Tag","name":"CodeGroup","attributes":{"mode":"tabs"},"children":[{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"python","data-title":"Python","header":{"title":"Python","controls":{"copy":{}}},"source":"import ipaddress\n\ndef ip_binding_range(ip: str) -> dict:\n    address = ipaddress.ip_address(ip)\n    if address.version == 6 and address.ipv4_mapped:\n        address = address.ipv4_mapped\n    prefix = 24 if address.version == 4 else 64\n    network = ipaddress.ip_network(f\"{address}/{prefix}\", strict=False)\n    return {\"method\": \"range\", \"family\": f\"v{address.version}\", \"value\": str(network)}\n","lang":"python"},"children":[]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"javascript","data-title":"Node.js","header":{"title":"Node.js","controls":{"copy":{}}},"source":"// Uses normaliseIp() from the hashing example above.\nfunction ipBindingRange(ip) {\n    const { family, address } = normaliseIp(ip);\n    if (family === 'v4') {\n        return { method: 'range', family, value: address.replace(/\\.\\d+$/, '.0') + '/24' };\n    }\n    // Write out all 8 groups, keep the first 4 and set the rest to zero.\n    const [head, tail = ''] = address.split('::');\n    const headGroups = head ? head.split(':') : [];\n    const tailGroups = tail ? tail.split(':') : [];\n    const zeros = Array(8 - headGroups.length - tailGroups.length).fill('0');\n    const groups = [...headGroups, ...zeros, ...tailGroups];\n    const network = normaliseIp(groups.slice(0, 4).join(':') + '::').address;\n    return { method: 'range', family, value: `${network}/64` };\n}\n","lang":"javascript"},"children":[]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"php","data-title":"PHP","header":{"title":"PHP","controls":{"copy":{}}},"source":"// Uses parseIp() and formatIp() from the hashing example above.\nfunction ipBindingRange(string $ip): array {\n    $bytes = parseIp($ip);\n    $prefix = strlen($bytes) === 4 ? 24 : 64;\n    // Set every bit after the prefix to zero.\n    $network = substr($bytes, 0, $prefix / 8) . str_repeat(\"\\0\", strlen($bytes) - $prefix / 8);\n    return [\n        'method' => 'range',\n        'family' => strlen($bytes) === 4 ? 'v4' : 'v6',\n        'value' => formatIp($network) . '/' . $prefix,\n    ];\n}\n","lang":"php"},"children":[]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"java","data-title":"Java","header":{"title":"Java","controls":{"copy":{}}},"source":"// Uses parseIp() and formatIp() from the hashing example above.\npublic static Map<String, String> ipBindingRange(String ip) throws Exception {\n    byte[] bytes = parseIp(ip);\n    int prefix = bytes.length == 4 ? 24 : 64;\n    // Set every bit after the prefix to zero.\n    Arrays.fill(bytes, prefix / 8, bytes.length, (byte) 0);\n    return Map.of(\"method\", \"range\", \"family\", bytes.length == 4 ? \"v4\" : \"v6\", \"value\", formatIp(bytes) + \"/\" + prefix);\n}\n","lang":"java"},"children":[]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"swift","data-title":"Swift","header":{"title":"Swift","controls":{"copy":{}}},"source":"// Uses parseIp() and formatIp() from the hashing example above.\nfunc ipBindingRange(ip: String) throws -> [String: String] {\n    var bytes = try parseIp(ip)\n    let prefix = bytes.count == 4 ? 24 : 64\n    // Set every bit after the prefix to zero.\n    for i in (prefix / 8)..<bytes.count {\n        bytes[i] = 0\n    }\n    return [\"method\": \"range\", \"family\": bytes.count == 4 ? \"v4\" : \"v6\", \"value\": \"\\(formatIp(bytes))/\\(prefix)\"]\n}\n","lang":"swift"},"children":[]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"ruby","data-title":"Ruby","header":{"title":"Ruby","controls":{"copy":{}}},"source":"require 'ipaddr'\n\ndef ip_binding_range(ip)\n  raise ArgumentError, \"Invalid IP address: #{ip}\" if ip.include?('/')\n  address = IPAddr.new(ip)\n  address = address.native if address.ipv4_mapped?\n  prefix = address.ipv4? ? 24 : 64\n  { method: 'range', family: address.ipv4? ? 'v4' : 'v6', value: \"#{address.mask(prefix)}/#{prefix}\" }\nend\n","lang":"ruby"},"children":[]}]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"related","__idx":4},"children":["Related"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/hosted-checkout/docs/api-integration/create-buy-order"},"children":["Create Buy Order"]}," and ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/hosted-checkout/docs/api-integration/create-sell-order"},"children":["Create Sell Order"]},": the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ipBinding"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ipBindingOmitted"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["originalUrl"]}," fields"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/hosted-checkout/docs/reference/error-codes#ip-binding"},"children":["Error Codes"]},": ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["IP_BINDING_MISSING"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/hosted-checkout/docs/transaction-lifecycle/webhooks#ip-binding-cancellations"},"children":["Webhooks"]},": ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["reason_code"]}," on cancelled orders"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/hosted-checkout/docs/going-live/overview"},"children":["Going Live"]}]}]}]},"headings":[{"value":"IP Binding","id":"ip-binding","depth":1},{"value":"Add IP binding to your integration","id":"add-ip-binding-to-your-integration","depth":2},{"value":"Hashing the IP address","id":"hashing-the-ip-address","depth":2},{"value":"Building a range","id":"building-a-range","depth":2},{"value":"Related","id":"related","depth":2}],"frontmatter":{"seo":{"title":"IP Binding: Send the Customer's IP Address | Banxa Hosted Checkout","description":"Send the customer's IP address with POST /v2/buy and POST /v2/sell so Banxa can check checkout is opened from the same network. Steps, hashing, ranges and errors."}},"lastModified":"2026-11-12T09:15:09.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/products/hosted-checkout/docs/api-integration/ip-binding","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}