Apple Pay Payouts API Implementation
Apple Pay JavaScript
1 (function () { 2 function centsToDollarsString(cents) { 3 if (typeof cents !== "number" || isNaN(cents)) return "0.00"; 4 return (cents / 100).toFixed(2); 5 } 6 7 function getHostname(defaultHostname) { 8 try { 9 return window.location && window.location.hostname 10 ? window.location.hostname 11 : defaultHostname || ""; 12 } catch (_) { 13 return defaultHostname || ""; 14 } 15 } 16 17 function ensureEnvironment() { 18 if (typeof window === "undefined") { 19 throw new Error("Apple Pay Disbursement must run in a browser context."); 20 } 21 if (!("PaymentRequest" in window)) { 22 throw new Error("Payment Request API not available in this browser."); 23 } 24 if ( 25 !window.customElements || 26 !("apple-pay-button" in document.createElement("div")) 27 ) { 28 // The custom element is provided by Apple’s SDK; we’ll only warn here. 29 } 30 return true; 31 } 32 33 async function validateMerchantSession( 34 validateMerchanturl, 35 domainName, 36 merchantId 37 ) { 38 const url = new URL(validateMerchanturl); 39 url.searchParams.set("domainName", domainName); 40 url.searchParams.set("merchantId", merchantId); 41 42 const res = await fetch(url.toString(), { 43 method: "GET", 44 credentials: "same-origin", 45 headers: { 46 Accept: "application/json", 47 }, 48 }); 49 if (!res.ok) { 50 const text = await res.text().catch(() => ""); 51 throw new Error("Merchant validation failed: " + res.status + " " + text); 52 } 53 return res.json(); 54 } 55 56 function buildPaymentMethodData(options) { 57 const merchantCapabilities = Array.isArray(options.merchantCapabilities) 58 ? options.merchantCapabilities 59 : ["supports3DS", "supportsInstantFundsOut"]; 60 61 const supportedNetworks = Array.isArray(options.supportedNetworks) 62 ? options.supportedNetworks 63 : ["masterCard", "visa"]; 64 65 const countryCode = options.countryCode || "US"; 66 67 return [ 68 { 69 supportedMethods: "https://apple.com/apple-pay", 70 data: { 71 version: 3, 72 merchantIdentifier: options.merchantIdentifier, 73 merchantCapabilities, 74 supportedNetworks, 75 countryCode, 76 }, 77 }, 78 ]; 79 } 80 81 function buildPaymentDetails(options) { 82 const currency = options.currency || "USD"; 83 const label = options.label || "Withdrawal"; 84 85 // Required order per Apple: Total Amount, [optional items...], IFO Fee, Disbursement Amount 86 const additionalLineItems = []; 87 additionalLineItems.push({ 88 label: "Total Amount", 89 amount: centsToDollarsString(options.grossAmountCents), 90 }); 91 92 // Optional additional line items can be inserted here if desired by callers 93 if (Array.isArray(options.extraLineItems)) { 94 for (var i = 0; i < options.extraLineItems.length; i++) { 95 var item = options.extraLineItems[i]; 96 if (!item || typeof item.label !== "string") continue; 97 additionalLineItems.push({ 98 label: item.label, 99 amount: String(item.amount), 100 }); 101 } 102 } 103 104 // IFO fee (required if supportsInstantFundsOut is in merchantCapabilities; can be 0.00) 105 if (typeof options.instantFundsOutFeeCents === "number") { 106 additionalLineItems.push({ 107 label: "Instant Transfer Fee", 108 amount: centsToDollarsString(options.instantFundsOutFeeCents), 109 disbursementLineItemType: "instantFundsOutFee", 110 }); 111 } 112 113 // Disbursement amount (required) 114 additionalLineItems.push({ 115 label: "Disbursement Amount", 116 amount: centsToDollarsString(options.totalAmountCents), 117 disbursementLineItemType: "disbursement", 118 }); 119 120 var disbursementRequest = {}; 121 if ( 122 Array.isArray(options.requiredRecipientContactFields) && 123 options.requiredRecipientContactFields.length > 0 124 ) { 125 disbursementRequest.requiredRecipientContactFields = 126 options.requiredRecipientContactFields; 127 } 128 129 return { 130 total: { 131 label: label, 132 amount: { 133 value: centsToDollarsString(options.totalAmountCents), 134 currency: currency, 135 }, 136 }, 137 modifiers: [ 138 { 139 supportedMethods: "https://apple.com/apple-pay", 140 data: { 141 disbursementRequest: disbursementRequest, 142 additionalLineItems: additionalLineItems, 143 }, 144 }, 145 ], 146 }; 147 } 148 149 function buildPaymentOptions() { 150 // PaymentOptions are ignored for Disbursement Request API; pass empty object. 151 return {}; 152 } 153 154 function attachClickHandler(buttonEl, handler) { 155 if (!buttonEl) return function () {}; 156 buttonEl.addEventListener("click", handler); 157 return function detach() { 158 buttonEl.removeEventListener("click", handler); 159 }; 160 } 161 162 function onClickFactory(options) { 163 return async function onApplePayButtonClicked() { 164 if (!("PaymentRequest" in window)) { 165 console.warn("Payment Request API not available."); 166 return; 167 } 168 169 var paymentMethodData = buildPaymentMethodData(options); 170 var paymentDetails = buildPaymentDetails(options); 171 var paymentOptions = buildPaymentOptions(); 172 173 var request = new PaymentRequest( 174 paymentMethodData, 175 paymentDetails, 176 // @ts-ignore - PaymentOptions ignored for Disbursement Request API 177 paymentOptions 178 ); 179 180 // Merchant Validation 181 // Browser invokes when request.show() is called 182 // We must fetch a merchant session from our server and complete the event with the promise 183 // Reference: https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/requesting_an_apple_pay_payment_session 184 // @ts-ignore 185 request.onmerchantvalidation = function (event) { 186 try { 187 var domainName = options.domainName || getHostname(""); 188 var merchantSessionPromise = validateMerchantSession( 189 options.coinflowApiUrl + 190 (options.validateMerchantPath || 191 "/api/checkout/apple-pay/validatemerchant"), 192 domainName, 193 options.merchantId 194 ); 195 event.complete(merchantSessionPromise); 196 } catch (err) { 197 console.error("Error starting merchant validation", err); 198 } 199 }; 200 201 // Keep modifiers from being reset by passing them back in the update 202 // @ts-ignore 203 request.onpaymentmethodchange = function (event) { 204 try { 205 var paymentDetailsUpdate = { 206 total: paymentDetails.total, 207 modifiers: paymentDetails.modifiers, 208 }; 209 event.updateWith(paymentDetailsUpdate); 210 } catch (err) { 211 console.error("onpaymentmethodchange error", err); 212 } 213 }; 214 215 try { 216 var response = await request.show(); 217 // At this point, the user authorized with Face ID/Touch ID/passcode 218 // response.details includes the encrypted Apple Pay token 219 if (typeof options.onSuccess === "function") { 220 try { 221 options.onSuccess(response.details); 222 } catch (_) {} 223 } 224 var status = "success"; 225 await response.complete(status); 226 } catch (err) { 227 if (typeof options.onError === "function") { 228 try { 229 options.onError(err); 230 } catch (_) {} 231 } 232 console.error("Apple Pay Disbursement failed", err); 233 } 234 }; 235 } 236 237 var api = { 238 init: function init(options) { 239 ensureEnvironment(); 240 if (!options || !options.merchantIdentifier) { 241 throw new Error("merchantIdentifier is required"); 242 } 243 if (!options || !options.merchantId) { 244 throw new Error("merchantId is required for backend validation"); 245 } 246 if (typeof options.totalAmountCents !== "number") { 247 throw new Error("totalAmountCents (number) is required"); 248 } 249 if (typeof options.grossAmountCents !== "number") { 250 throw new Error("grossAmountCents (number) is required"); 251 } 252 253 var selector = options.buttonSelector || "apple-pay-button"; 254 var buttonEl = document.querySelector(selector); 255 if (!buttonEl) { 256 throw new Error("Apple Pay button not found for selector: " + selector); 257 } 258 259 var onClick = onClickFactory(options); 260 var detach = attachClickHandler(buttonEl, onClick); 261 262 return { 263 detach: detach, 264 }; 265 }, 266 }; 267 268 // Expose to window 269 window.CoinflowApplePayDisbursement = api; 270 })();
Apple Pay TypeScript
1 (function () { 2 function centsToDollarsString(cents: number) { 3 if (typeof cents !== "number" || isNaN(cents)) return "0.00"; 4 return (cents / 100).toFixed(2); 5 } 6 7 function getHostname(defaultHostname: string) { 8 try { 9 return window.location && window.location.hostname 10 ? window.location.hostname 11 : defaultHostname || ""; 12 // eslint-disable-next-line @typescript-eslint/no-unused-vars 13 } catch (_: any) { 14 return defaultHostname || ""; 15 } 16 } 17 18 function ensureEnvironment() { 19 if (typeof window === "undefined") { 20 throw new Error("Apple Pay Disbursement must run in a browser context."); 21 } 22 if (!("PaymentRequest" in window)) { 23 throw new Error("Payment Request API not available in this browser."); 24 } 25 if ( 26 !window.customElements || 27 !("apple-pay-button" in document.createElement("div")) 28 ) { 29 // The custom element is provided by Apple’s SDK; we’ll only warn here. 30 } 31 return true; 32 } 33 34 async function validateMerchantSession( 35 validateMerchanturl: string, 36 domainName: string, 37 merchantId: string 38 ) { 39 const url = new URL(validateMerchanturl); 40 url.searchParams.set("domainName", domainName); 41 url.searchParams.set("merchantId", merchantId); 42 43 const res = await fetch(url.toString(), { 44 method: "GET", 45 credentials: "same-origin", 46 headers: { 47 Accept: "application/json", 48 }, 49 }); 50 if (!res.ok) { 51 const text = await res.text().catch(() => ""); 52 throw new Error("Merchant validation failed: " + res.status + " " + text); 53 } 54 return res.json(); 55 } 56 57 function buildPaymentMethodData(options: Record<string, any>) { 58 const merchantCapabilities = Array.isArray(options.merchantCapabilities) 59 ? options.merchantCapabilities 60 : ["supports3DS", "supportsInstantFundsOut"]; 61 62 const supportedNetworks = Array.isArray(options.supportedNetworks) 63 ? options.supportedNetworks 64 : ["masterCard", "visa"]; 65 66 const countryCode = options.countryCode || "US"; 67 68 return [ 69 { 70 supportedMethods: "https://apple.com/apple-pay", 71 data: { 72 version: 3, 73 merchantIdentifier: options.merchantIdentifier, 74 merchantCapabilities, 75 supportedNetworks, 76 countryCode, 77 }, 78 }, 79 ]; 80 } 81 82 function buildPaymentDetails(options: Record<string, any>) { 83 const currency = options.currency || "USD"; 84 const label = options.label || "Withdrawal"; 85 86 // Required order per Apple: Total Amount, [optional items...], IFO Fee, Disbursement Amount 87 const additionalLineItems: { 88 label: string; 89 amount: string; 90 disbursementLineItemType?: string; 91 }[] = []; 92 additionalLineItems.push({ 93 label: "Total Amount", 94 amount: centsToDollarsString(options.grossAmountCents), 95 }); 96 97 // Optional additional line items can be inserted here if desired by callers 98 if (Array.isArray(options.extraLineItems)) { 99 for (let i = 0; i < options.extraLineItems.length; i++) { 100 const item = options.extraLineItems[i]; 101 if (!item || typeof item.label !== "string") continue; 102 additionalLineItems.push({ 103 label: item.label, 104 amount: String(item.amount), 105 }); 106 } 107 } 108 109 // IFO fee (required if supportsInstantFundsOut is in merchantCapabilities; can be 0.00) 110 if (typeof options.instantFundsOutFeeCents === "number") { 111 additionalLineItems.push({ 112 label: "Instant Transfer Fee", 113 amount: centsToDollarsString(options.instantFundsOutFeeCents), 114 disbursementLineItemType: "instantFundsOutFee", 115 }); 116 } 117 118 // Disbursement amount (required) 119 additionalLineItems.push({ 120 label: "Disbursement Amount", 121 amount: centsToDollarsString(options.totalAmountCents), 122 disbursementLineItemType: "disbursement", 123 }); 124 125 const disbursementRequest: { requiredRecipientContactFields?: string[] } = 126 {}; 127 if ( 128 Array.isArray(options.requiredRecipientContactFields) && 129 options.requiredRecipientContactFields.length > 0 130 ) { 131 disbursementRequest.requiredRecipientContactFields = 132 options.requiredRecipientContactFields; 133 } 134 135 return { 136 total: { 137 label: label, 138 amount: { 139 value: centsToDollarsString(options.totalAmountCents), 140 currency: currency, 141 }, 142 }, 143 modifiers: [ 144 { 145 supportedMethods: "https://apple.com/apple-pay", 146 data: { 147 disbursementRequest: disbursementRequest, 148 additionalLineItems: additionalLineItems, 149 }, 150 }, 151 ], 152 }; 153 } 154 155 function buildPaymentOptions() { 156 // PaymentOptions are ignored for Disbursement Request API; pass empty object. 157 return {}; 158 } 159 160 function attachClickHandler( 161 buttonEl: HTMLButtonElement, 162 handler: (this: HTMLButtonElement, ev: PointerEvent) => any 163 ) { 164 if (!buttonEl) return function () {}; 165 buttonEl.addEventListener("click", handler); 166 return function detach() { 167 buttonEl.removeEventListener("click", handler); 168 }; 169 } 170 171 function onClickFactory(options: Record<string, any>) { 172 return async function onApplePayButtonClicked() { 173 if (!("PaymentRequest" in window)) { 174 console.warn("Payment Request API not available."); 175 return; 176 } 177 178 const paymentMethodData = buildPaymentMethodData(options); 179 const paymentDetails = buildPaymentDetails(options); 180 const paymentOptions = buildPaymentOptions(); 181 182 const request = new PaymentRequest( 183 paymentMethodData, 184 paymentDetails, 185 paymentOptions 186 ); 187 188 // Merchant Validation 189 // Browser invokes when request.show() is called 190 // We must fetch a merchant session from our server and complete the event with the promise 191 // Reference: https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/requesting_an_apple_pay_payment_session 192 // @ts-expect-error - onmerchantvalidation is not a valid property on PaymentRequest 193 request.onmerchantvalidation = function (event) { 194 try { 195 const domainName = options.domainName || getHostname(""); 196 const merchantSessionPromise = validateMerchantSession( 197 options.coinflowApiUrl + 198 (options.validateMerchantPath || 199 "/api/checkout/apple-pay/validatemerchant"), 200 domainName, 201 options.merchantId 202 ); 203 event.complete(merchantSessionPromise); 204 } catch (err) { 205 console.error("Error starting merchant validation", err); 206 } 207 }; 208 209 // Keep modifiers from being reset by passing them back in the update 210 request.onpaymentmethodchange = function (event) { 211 try { 212 const paymentDetailsUpdate = { 213 total: paymentDetails.total, 214 modifiers: paymentDetails.modifiers, 215 }; 216 event.updateWith(paymentDetailsUpdate); 217 } catch (err) { 218 console.error("onpaymentmethodchange error", err); 219 } 220 }; 221 222 try { 223 const response = await request.show(); 224 // At this point, the user authorized with Face ID/Touch ID/passcode 225 // response.details includes the encrypted Apple Pay token 226 if (typeof options.onSuccess === "function") { 227 try { 228 options.onSuccess(response.details); 229 } catch (e) { 230 console.error("onSuccess error", e); 231 } 232 } 233 await response.complete("success"); 234 } catch (err) { 235 if (typeof options.onError === "function") { 236 try { 237 options.onError(err); 238 } catch (e) { 239 console.error("onError error", e); 240 } 241 } 242 console.error("Apple Pay Disbursement failed", err); 243 } 244 }; 245 } 246 247 const api = { 248 init: function init(options: Record<string, any>) { 249 ensureEnvironment(); 250 if (!options || !options.merchantIdentifier) { 251 throw new Error("merchantIdentifier is required"); 252 } 253 if (!options || !options.merchantId) { 254 throw new Error("merchantId is required for backend validation"); 255 } 256 if (typeof options.totalAmountCents !== "number") { 257 throw new Error("totalAmountCents (number) is required"); 258 } 259 if (typeof options.grossAmountCents !== "number") { 260 throw new Error("grossAmountCents (number) is required"); 261 } 262 263 const selector = options.buttonSelector || "apple-pay-button"; 264 const buttonEl = document.querySelector(selector); 265 if (!buttonEl) { 266 throw new Error("Apple Pay button not found for selector: " + selector); 267 } 268 269 const onClick = onClickFactory(options); 270 const detach = attachClickHandler(buttonEl, onClick); 271 272 return { 273 detach: detach, 274 }; 275 }, 276 }; 277 278 // Expose to window 279 (window as any).CoinflowApplePayDisbursement = api; 280 })();
Apple Pay Display
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8" /> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 <title>Document</title> 7 <script 8 crossorigin 9 src="https://applepay.cdn-apple.com/jsapi/1.latest/apple-pay-sdk.js" 10 ></script> 11 <style> 12 apple-pay-button { 13 --apple-pay-button-width: 200px; 14 --apple-pay-button-height: 40px; 15 --apple-pay-button-border-radius: 6px; 16 --apple-pay-button-padding: 0 0; 17 --apple-pay-button-box-sizing: border-box; 18 } 19 </style> 20 </head> 21 <body> 22 <apple-pay-button 23 id="apple-pay-disburse" 24 buttonstyle="black" 25 type="continue" 26 locale="en-US" 27 ></apple-pay-button> 28 <script src="/apple-pay-disbursement.js"></script> 29 <script> 30 CoinflowApplePayDisbursement.init({ 31 buttonSelector: "#apple-pay-disburse", 32 // Apple Pay merchant identifier (from Apple) 33 merchantIdentifier: "merchant.main.coinflow", 34 // Your internal merchantId used by your backend validation endpoint 35 merchantId: "YOUR_MERCHANT_ID", 36 // Optional override; defaults to window.location.hostname 37 domainName: undefined, 38 coinflowApiUrl: "https://api-sandbox.coinflow.cash", 39 // API endpoint in your backend that performs Apple merchant validation 40 validateMerchantPath: "/api/checkout/apple-pay/validatemerchant", 41 42 // Disbursement amounts in cents (USD) 43 label: "Withdrawal", 44 totalAmountCents: 1100, // e.g. transfer amount shown as the sheet total 45 grossAmountCents: 1200, // "Total Amount" 46 instantFundsOutFeeCents: 100, // IFO fee (set 0 for no fee) 47 48 // Recipient info you want to collect for disbursement (optional) 49 requiredRecipientContactFields: ["name", "postalAddress"], 50 51 onSuccess: function (details) { 52 console.log("Disbursement authorized. Apple Pay token:", details); 53 // Send token/details to Coinflow's Withdraw Transaction endpoint 54 const options = { 55 method: 'POST', 56 headers: {accept: 'application/json', 'content-type': 'application/json'}, 57 body: JSON.stringify({speed: 'asap'}), 58 token: details 59 }; 60 61 fetch('https://api-sandbox.coinflow.cash/api/withdraw/transaction', options) 62 .then(res => res.json()) 63 .then(res => console.log(res)) 64 .catch(err => console.error(err)); 65 }, 66 67 onError: function (err) { 68 console.error("Apple Pay Disbursement error", err); 69 }, 70 }); 71 </script> 72 </body> 73 </html>
Response Example
1 {"success":true}
Include Apple Pay SDK in HTML script
In order to utilize the Apple Pay SDK, include this script.
Display the Apple Pay Button
From the Apple Pay SDK, display the predefined apple-pay-button element.
Add the CoinflowApplePayDisbursment API function
Either make a separate file for this disbursement request, or include this in your code where it feels most logical. This example JavaScript/TypeScript code allows Coinflow to take params from the Apple Pay button and create an Apple Pay disbursement request.
Include the CoinflowApplePayDisbursement request script in your frontend
Other than the buttonSelector, coinflowApiUrl, and validateMerchantPath, configure these parameters to match up with the details of the withdraw request.
Call Coinflow’s Create Withdraw Transaction Endpoint
Once the disbursement request to Apple’s API comes through as a success, you will pass the received token object into Coinflows API endpoint to initiate the payout.

