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.