# ACH Save Source: https://docs.pushcash.com/ach-save ## Overview ACH Save recovers eligible declined debit card transactions by retrying them over ACH. Many card declines are recoverable — issuers routinely decline legitimate transactions due to outdated risk rules, expired cards, or system errors. ACH Save lets the user re-authorize the same purchase through an authenticated bank flow. ### How it works 1. Invoke ACH save from your existing card processing flow when an eligible transaction is declined. 2. Push evaluates the transaction's risk and, if approved, returns a link where the user can login to their bank. 3. After the user authenticates and links their checking account, Push re-authorizes the transaction and originates the payment over ACH. ACH Save is a pilot program on eligible BIN ranges and risk thresholds. Reach out to your Push Cash representative for details about what transactions qualify for ACH Saves. ## Integration overview The steps below show an overview of how to integrate ACH Save. 1. **Register the user.** Call the [create-user](./apireference/user/create-user) endpoint with the user's name, email, address, and phone number. * Retain the returned user `id` for the Authorization call (see step 3) * Register each user **only once** 2. **Generate a token.** Call the [tokenize-card](./apireference/tokenization/tokenize-card) endpoint with the user's `pan` * Retain the returned `token` for the Authorization call (see step 3) * To avoid sharing the full card number, mask the middle digits 8–12 with `XXXX` and only transmit the BIN and last four of the user's card. 3. **Authorize the payment.** Call the [authorize-payment](./apireference/authorization/authorize-payment) endpoint with the `amount`, `currency`, `direction: cash_in`, `type: ach_save`, `user_id`, the `token` from the previous step, and a `redirect_url`. Handle the response by its HTTP status code: * **`202 Accepted`** — the user must complete bank authentication. Persist the returned intent `id`, then continue to step 4 using the returned `url`. * **`200 OK`** — the payment was approved immediately (i.e. for a returning user whose bank is already linked). Notify the user that the deposit succeeded. * **`401 Unauthorized`** — the payment was declined (e.g. due to transaction risk) 4. **Direct the user to complete bank authentication** Navigate the user to the `url` returned from the Authorize call. When the flow completes, the user is returned to your application via the `redirect_url` you set on the authorize request. 5. **Retrieve the ACH authorization result.** Call the [get-an-intent](./apireference/intent/get-an-intent) endpoint and inspect the `status` field. * If `status` is `approved`, Push authorized the payment and automatically originates it over ACH. Update your internal transaction record and display a message to the user that the payment succeeded. * If `status` is `declined`, Push could not approve the payment for ACH (e.g. due to insufficient balance). ```bash Register User theme={null} curl --request POST \ --url https://sandbox.pushcash.com/user \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "name": { "first": "Alfred", "last": "Hitchcock" }, "email": "alfred@imdb.com", "address": { "address_line_1": "1609 10th Ave", "locality": "Bodega Bay", "administrative_area": "CA", "postal_code": "94923", "country": "US" }, "phone_number": "(555) 681-3485", "tag": "4c8e6b4f" } ' ``` ```bash Generate Token theme={null} curl --request POST \ --url https://sandbox-tokens.pushcash.com/tokenize \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "pan": "55555555XXXX4444" } ' ``` ```bash Authorize Payment theme={null} curl --request POST \ --url https://sandbox.pushcash.com/authorize \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "amount": 45000, "currency": "USD", "direction": "cash_in", "type": "ach_save", "user_id": "user_lVpbPL0K1XIiHx0DxipRbD", "token": "token_mbDRHFi3dxIZEtykHsgUGC", "redirect_url": "https://yourapp.com/checkout/?29080389002" } ' ``` ```bash Retrieve ACH Authorization Result theme={null} curl --request GET \ --url https://sandbox.pushcash.com/intent/intent_sandbox_dMggQ93ZYH6DH9LBhVeijE \ --header 'Authorization: Bearer ' ``` ```json Register User - 200 OK theme={null} { "id": "user_lVpbPL0K1XIiHx0DxipRbD" } ``` ```json Generate Token - 200 OK theme={null} { "token": "token_mbDRHFi3dxIZEtykHsgUGC" } ``` ```json Authorize - 202 Accepted (Authentication Required) theme={null} { "id": "intent_sandbox_dMggQ93ZYH6DH9LBhVeijE", "url": "https://cdn.pushcash.com/ux/intent_sandbox_dMggQ93ZYH6DH9LBhVeijE" } ``` ```json Authorize - 200 OK (Approved) theme={null} { "id": "intent_sandbox_dMggQ93ZYH6DH9LBhVeijE", "amount": 45000, "direction": "cash_in", "currency": "USD", "credential": { "id": "cred_7YlA9IiSl8UZvNwNSbFajV", "display_name": "Chase Checking", "last4": "6018" } } ``` ```json Authorize - 401 Unauthorized (Declined) theme={null} { "id": "intent_sandbox_dMggQ93ZYH6DH9LBhVeijE", "decline_category": "insufficient_funds" } ``` ```json Retrieve Result - Approved theme={null} { "id": "intent_sandbox_dMggQ93ZYH6DH9LBhVeijE", "status": "approved" } ``` ```json Retrieve Result - Declined theme={null} { "id": "intent_sandbox_dMggQ93ZYH6DH9LBhVeijE", "status": "declined" } ``` ## Next steps Set up webhooks to receive `intent.approved` and decline notifications asynchronously. See the [enabling webhooks](./enabling-webhooks) guide. # API Concepts Source: https://docs.pushcash.com/api This guide outlines important concepts to consider when developing an integration with the Push Cash API. ## Environments Push Cash maintains two separate environments: | Environment | Base URL | Purpose | | ----------- | ------------------------------ | --------------------------------- | | Sandbox | `https://sandbox.pushcash.com` | Testing and development | | Production | `https://api.pushcash.com` | Live transactions with real funds | Both environments expose the same API surface. Use the sandbox environment to build and test your integration before switching to production. ## Authentication The API uses a persistent API key to authenticate requests. Provide the key using the `Authorization` header with the value `Bearer YOUR_API_KEY`. Requests that fail authentication return a `401` (Unauthorized) status code. In order to test your API keys, you can make a request to the `/keys/verify` endpoint in either sandbox or production ```shell theme={null} curl -X POST -H "Authorization: Bearer $APIKEY" https://sandbox.pushcash.com/keys/verify ``` If the API key is valid, the API will respond with a status code of `200` (OK) and the name of your organization ## IP Allowlisting As an optional, additional layer of security, Push Cash can restrict access to your account so that **write requests** are only accepted from a set of source IP addresses that you specify. This protects your account even if an API key is leaked, since a stolen key cannot be used to move money from an unrecognized network. IP allowlisting is configured by Push Cash during onboarding (or at any time afterward) — it is not self-serve. To enable it or change your allowlisted IPs, contact your Push Cash representative or [support](mailto:hello@pushcash.com) with the list of source IP addresses your backend uses to reach the API. IP allowlisting applies **only to write operations** — `POST`, `PUT`, `PATCH`, and `DELETE` requests (for example, authorizing a payment or creating a user). Read operations (`GET`, `HEAD`) are **not** restricted and can be made from any IP address, so dashboards, monitoring, and reconciliation jobs continue to work without being added to the allowlist. ### How it works * You provide Push Cash with **one or more** source IP addresses. Multiple addresses are supported, which is useful when your traffic egresses from several NAT gateways or regions. * When the feature is enabled, every write request is checked against your allowlist. The request must originate from one of the configured addresses. * A write request from an IP that is **not** on the allowlist is rejected with a `403` (Forbidden) status code. Read requests are never blocked by this check. * If no addresses are configured for your account, the check is skipped and all requests are allowed. ### Best practices * Allowlist the **stable egress IPs** of your backend (for example, the public IPs of your NAT gateways or load balancers), not the IPs of individual servers that may change. * Provide every egress IP your traffic can come from. If your platform scales across multiple gateways or availability zones, include all of them to avoid intermittent `403` rejections. * Notify Push Cash **before** changing your network egress (for example, migrating regions or adding a new gateway) so your allowlist can be updated ahead of the cutover. ## Idempotency Idempotency ensures that making the same request more than once won’t result in duplicate operations. This is helpful in cases like network retries or client timeouts. When a `POST` request is received, we determine whether it’s a duplicate by comparing the `tag` field in the request body against previous requests. Set the `tag` to your own internal identifier for the resource. If we detect a duplicate, we return the **same response as the original request**, and do **not** create the resource again. ### Idempotency by Endpoint | Endpoint | Idempotency Mechanism | Description | | ----------------- | --------------------- | ----------------------------------------------------- | | `POST /user` | `tag` field | Requests with the same `tag` return the same user. | | `POST /authorize` | `tag` field | Requests with the same `tag` return the same payment. | ### Best Practices * Set the `tag` field to your internal identifier for the resource so that retries map back to the same record. * Use a unique `tag` for each new resource, and reuse the same `tag` when retrying a request. ## Rate Limiting The Push Cash API rate-limits requests to ensure stable and reliable service for all users. All rate limits are evaluated on a sliding 1 minute window. Requests subject to rate limiting will include the following response headers: * `X-RateLimit-Limit`: The maximum number of requests that can be made to the endpoint in a window. * `X-RateLimit-Remaining`: The number of requests remaining in the current window. * `X-RateLimit-Reset`: The time at which the current window will reset. When a request exceeds the rate limit, the API will respond with a status code of `429` (Too Many Requests). Requests that are issued from your backend and authenticated with an API token are subject to the following rate limits: | Operation | Examples | Limit | | --------- | ----------------------------------- | -------------------- | | Write | Authorizing payments | 100 requests / min | | Read | Getting or listing existing objects | 1,000 requests / min | # Authorize payment Source: https://docs.pushcash.com/apireference/authorization/authorize-payment /openapi.yaml post /authorize Submit a payment request directly to the Push Authorization Engine. # List disputes Source: https://docs.pushcash.com/apireference/dispute/list-disputes /openapi.yaml get /dispute/list Retrieves a list of disputes # Retrieve a dispute Source: https://docs.pushcash.com/apireference/dispute/retrieve-a-dispute /openapi.yaml get /dispute/{id} Retrieves a specific dispute by ID # Approve a pending intent Source: https://docs.pushcash.com/apireference/intent/approve-a-pending-intent /openapi.yaml post /intent/{id}/approve For intents which must be approved by the operator manually, approves the intent and posts the payment to the network. # Cancel an intent Source: https://docs.pushcash.com/apireference/intent/cancel-an-intent /openapi.yaml post /intent/{id}/cancel Cancel an outstanding intent which has not yet been approved. # Get an intent Source: https://docs.pushcash.com/apireference/intent/get-an-intent /openapi.yaml get /intent/{id} Get an intent by ID # List intents Source: https://docs.pushcash.com/apireference/intent/list-intents /openapi.yaml get /intent/list Retrieves a list of intents # Get a transaction Source: https://docs.pushcash.com/apireference/ledger/get-a-transaction /openapi.yaml get /transaction/{id} Retrieves a specific transaction by its ID. # Get account Source: https://docs.pushcash.com/apireference/ledger/get-account /openapi.yaml get /account/{id} Retrieves a single account and balance by ID # List accounts Source: https://docs.pushcash.com/apireference/ledger/list-accounts /openapi.yaml get /account/list Retrieves a list of all accounts # List transactions Source: https://docs.pushcash.com/apireference/ledger/list-transactions /openapi.yaml get /transaction/list Retrieves a list of transactions # List transfers Source: https://docs.pushcash.com/apireference/ledger/list-transfers /openapi.yaml get /transfer/list Retrieves a list of transfers. # Retrieve a transfer Source: https://docs.pushcash.com/apireference/ledger/retrieve-a-transfer /openapi.yaml get /transfer/{id} Retrieves a specific transfer by its ID. # Create a refund Source: https://docs.pushcash.com/apireference/refund/create-a-refund /openapi.yaml post /refund Create a refund or partial refund for an approved intent # Get a refund Source: https://docs.pushcash.com/apireference/refund/get-a-refund /openapi.yaml get /refund/{id} Retrieves a specific refund by ID # List refunds Source: https://docs.pushcash.com/apireference/refund/list-refunds /openapi.yaml get /refund/list Retrieves a list of refunds # Simulate a stored credential Source: https://docs.pushcash.com/apireference/simulation/simulate-a-stored-credential /openapi.yaml post /sandbox/credential **Sandbox only.** Creates a stored credential of the specified type. The credential may be used for `cash_in` and `cash_out` transactions. This endpoint is not available in the production environment. # Tokenize card Source: https://docs.pushcash.com/apireference/tokenization/tokenize-card /openapi.yaml post /tokenize Exchange raw cardholder data for a short-lived token that can be passed to [authorize-payment](/apireference/authorization/authorize-payment). This endpoint is hosted on a PCI-isolated domain and must be called directly with the cardholder's PAN, CVV, and expiration date. Only operators with their own PCI DSS compliance should call this endpoint directly. # Create user Source: https://docs.pushcash.com/apireference/user/create-user /openapi.yaml post /user Register a user with Push # Create User URL Source: https://docs.pushcash.com/apireference/user/create-user-url /openapi.yaml post /user/{id}/url Generate a URL for the Push User Widget or Apple Pay SDK. - If `type` is omitted, generates a widget URL that supports all card processing types enabled for your instance. - If `type` is `card_only` or `secure_debit`, generates a widget URL restricted to that processing type. These values require the matching processing category to be enabled on your contract. - If `type` is `apple_pay`, generates a URL for use with the SDK's `ApplePay` launcher. # Get a user Source: https://docs.pushcash.com/apireference/user/get-a-user /openapi.yaml get /user/{id} Retrieve a user by ID # List user credentials Source: https://docs.pushcash.com/apireference/user/list-user-credentials /openapi.yaml get /user/{id}/credential/list Retrieves the user's stored credentials matching the requested `type` values. Requests that do not supply any `type` values return an empty list. # List users Source: https://docs.pushcash.com/apireference/user/list-users /openapi.yaml get /user/list Retrieves a list of users # Apple Pay Mobile Source: https://docs.pushcash.com/apple-pay-mobile ## Overview On iOS, your app presents the payment sheet directly with [PassKit](https://developer.apple.com/documentation/passkit) under your own Apple merchant ID. After the user authorizes, your app receives an encrypted `PKPaymentToken`; your backend exchanges it for a Push token with the tokens API and authorizes the payment. The Apple Pay payload is encrypted by Apple to a payment processing certificate held by Push. Your app and your servers forward it as ciphertext and never handle raw card numbers. ## Configuration Apple requires the merchant ID used in an app to belong to the Apple Developer team that signs the app. This is a one-time setup, completed together with Push Cash: 1. **Create a merchant ID in your Apple Developer account.** In [Certificates, Identifiers & Profiles](https://developer.apple.com/help/account/capabilities/configure-apple-pay), create a merchant identifier (for example, `merchant.com.your-company.cashier`). 2. **Add the Apple Pay capability to your app.** In Xcode, add the **Apple Pay** capability to your app target and select the merchant ID. This places it in the app's `com.apple.developer.in-app-payments` entitlement. 3. **Issue the payment processing certificate with Push.** Push generates the key pair and sends you a certificate signing request (CSR). Upload the CSR under your merchant ID in the Apple developer console (**Apple Pay Payment Processing Certificate**) and share the issued certificate with your Push Cash representative. Apple Pay payment processing certificates are valid for 25 months. Push will work with you to re-provision a fresh payment processing cert before expiration. ## Integration overview The steps below show an overview of how to accept an Apple Pay deposit from your iOS app. 1. **Register the user.** Call the [create-user](./apireference/user/create-user) endpoint with the user's name, email, address, and phone number. * Store the returned Push `id` alongside your internal user record. * Register each user **only once** and reuse the user's `id` on every subsequent transaction. 2. **Present the payment sheet.** Build a `PKPaymentRequest` with your merchant ID, `merchantCapabilities: [.threeDSecure]`, the card networks enabled for your account, and the payment amount, then present a `PKPaymentAuthorizationController`. See [Payment sheet](#payment-sheet). * Check `PKPaymentAuthorizationController.canMakePayments()` and render the Apple Pay button only when it returns `true`. For styling and placement, follow Apple's [design guidelines](https://developer.apple.com/design/human-interface-guidelines/apple-pay#Using-Apple-Pay-buttons). 3. **Tokenize the payment.** When the user authorizes with Face ID or Touch ID, your delegate receives a `PKPayment` containing the encrypted `PKPaymentToken`. Send it to your backend, and exchange it for a Push token by calling the [tokenize](./apireference/tokenization/tokenize-card) endpoint on the tokens domain with the user's `id`, `type: "apple_pay"`, and the serialized token as `apple_pay_token`. * The token's `transactionIdentifier` acts as an idempotency key — submitting the same `PKPaymentToken` twice returns the same Push token. 4. **Authorize the payment.** Pass the returned `token`, along with the `amount`, `currency`, and `direction: "cash_in"`, to the [authorize-payment](./apireference/authorization/authorize-payment) endpoint from your backend, then return the result to your app and complete the payment sheet by passing a `PKPaymentAuthorizationResult` to the delegate's completion handler. ```bash Tokenize Apple Pay theme={null} curl --request POST \ --url https://sandbox-tokens.pushcash.com/tokenize \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "user_id": "user_lVpbPL0K1XIiHx0DxipRbD", "type": "apple_pay", "apple_pay_token": { "paymentData": { "data": "", "header": { "ephemeralPublicKey": "", "publicKeyHash": "", "transactionId": "c8b4..." }, "signature": "", "version": "EC_v1" }, "paymentMethod": { "displayName": "Visa 4444", "network": "Visa", "type": "debit" }, "transactionIdentifier": "c8b4f9b2e6d14a5f9c3e7d0a1b2c3d4e" } } ' ``` ```bash Authorize Payment theme={null} curl --request POST \ --url https://sandbox.pushcash.com/authorize \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "amount": 1000, "currency": "USD", "direction": "cash_in", "token": "token_mbDRHFi3dxIZEtykHsgUGC" } ' ``` ```json Tokenize Apple Pay - 200 OK theme={null} { "token": "token_mbDRHFi3dxIZEtykHsgUGC" } ``` ```json Authorize Payment - Approved theme={null} { "id": "intent_sandbox_mbDRHFi3dxIZEtykHsgUGC", "status": "approved" } ``` ```json Authorize Payment - Declined theme={null} { "id": "intent_sandbox_mbDRHFi3dxIZEtykHsgUGC", "status": "declined" } ``` ## Payment sheet Present the payment sheet from your app with a `PKPaymentRequest`. The merchant ID must match your app's entitlement (see [Configuration](#configuration)). ```swift theme={null} import PassKit // Render the Apple Pay button only when the device can pay. guard PKPaymentAuthorizationController.canMakePayments() else { return } // On tap: build the request and present the sheet. let request = PKPaymentRequest() request.merchantIdentifier = "merchant.com.your-company.cashier" request.merchantCapabilities = [.threeDSecure] request.supportedNetworks = [.visa, .masterCard, .discover] request.countryCode = "US" request.currencyCode = "USD" request.paymentSummaryItems = [ PKPaymentSummaryItem(label: "Deposit", amount: NSDecimalNumber(string: "10.00")), ] let controller = PKPaymentAuthorizationController(paymentRequest: request) controller.delegate = self controller.present() ``` ## Withdrawals A returning user can withdraw funds to a debit card they previously deposited with via Apple Pay — no re-authentication through the payment sheet is required. Users must have completed at least one deposit through the flow above before a withdrawal credential is available. Withdraw to a stored Apple Pay debit card 1. **Display stored credentials.** Call the [list-user-credentials](/apireference/user/list-user-credentials) endpoint with the user's ID, passing `apple_pay_debit` as a `type` query parameter (`?type=apple_pay_debit`). Render the returned credentials so the user can pick which card to withdraw to. * Only deposits made with a **debit** card create an `apple_pay_debit` credential. Deposits made with a credit card do not create a withdrawal-eligible credential. 2. **Submit the withdrawal.** Call the [authorize-payment](/apireference/authorization/authorize-payment) endpoint with the selected `credential_id`, `direction: cash_out`, and the `amount`. * A `200` response means the withdrawal was approved — display the result to the user. * Handle a `401` response. In a small number of cases the debit card does not support **OCT** (Original Credit Transactions), which are required to push funds to a card. This is determined by the card issuer and cannot be resolved for that card — prompt the user to select or add a different debit card. ```bash List User Credentials theme={null} curl --request GET \ --url 'https://sandbox.pushcash.com/user/{id}/credentials?type=apple_pay_debit' \ --header 'Authorization: Bearer ' ``` ```bash Authorize Withdrawal theme={null} curl --request POST \ --url https://sandbox.pushcash.com/authorize \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "user_id": "user_lVpbPL0K1XIiHx0DxipRbD", "credential_id": "cred_sandbox_123", "amount": 2500, "currency": "USD", "direction": "cash_out" } ' ``` ```json List User Credentials - 200 OK theme={null} { "data": [ { "id": "cred_7YlA9IiSl8UZvNwNSbFajV", "created_at": "2023-05-24T20:15:18.158Z", "card_last4": "4444", "authenticated": true, "type": "apple_pay_debit", "account_last4": null, "bank_name": null } ] } ``` ## Sandbox testing Test the full flow against the sandbox host (`sandbox.pushcash.com`) before going live. To exercise the payment sheet on the **web**, deploy your sandbox cashier to a verified HTTPS domain (see [Domain registration](/apple-pay-web#domain-registration)) — Apple Pay cannot run on `localhost`. In an **app**, test on a physical device signed in to an [Apple sandbox tester account](https://developer.apple.com/apple-pay/sandbox-testing/) — the iOS simulator does not produce real encrypted payment data. In sandbox, simulate a declined authorization by submitting for `2200` cents (\$22.00). Any other amount is approved. This applies to both deposit authorizations and withdrawal OCT declines. ### Simulating a stored credential To test withdrawals without performing a real Apple Pay deposit, create a stored `apple_pay_debit` credential with synthetic card data using the [simulate-credential](/apireference/simulation/simulate-a-stored-credential) endpoint: ```bash theme={null} curl --request POST \ --url https://sandbox.pushcash.com/sandbox/credential \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "user_id": "user_lVpbPL0K1XIiHx0DxipRbD", "type": "apple_pay_debit", "card_last4": "9010" } ' ``` The returned credential behaves like any stored credential: it appears in [list-user-credentials](/apireference/user/list-user-credentials) and can be used to authorize `cash_out` payments. The endpoint is available in sandbox alone. ### Integration checklist Work through the following before requesting production access: * Simulate an approved deposit by submitting a payment for \$10.00 * Simulate a declined deposit by submitting a payment for \$22.00 * Create an `apple_pay_debit` credential using the [simulate-credential](/apireference/simulation/simulate-a-stored-credential) endpoint, verify it is returned by [list-user-credentials](/apireference/user/list-user-credentials) with `?type=apple_pay_debit` and displayed with `card_last4`, and test a withdrawal to it * Test an OCT decline by submitting a `cash_out` for `2200` cents (\$22.00) against an `apple_pay_debit` credential ## Next steps * Set up webhooks to receive asynchronous updates about payment status — see the [enabling webhooks](/enabling-webhooks) guide. * Move your integration from sandbox to production with the [Go-Live Checklist](/go-live-checklist). # Apple Pay Web Source: https://docs.pushcash.com/apple-pay-web ## Overview On the web, the Push SDK handles presenting the payment sheet and processing the payment data received from Apple after the user authenticates and completes the payment. Your integration is responsible for displaying the Apple Pay button, initializing the SDK, and supplying the resulting token to the Push API to authorize the payment. Apple Pay on the web requires your site to be served over HTTPS on a domain that Apple has verified. As a result, this integration cannot be tested on `localhost`, and each domain that displays the Apple Pay button must be verified first. See [Domain registration](#domain-registration) and Apple's [Setting up your server](https://developer.apple.com/documentation/applepayontheweb/setting-up-your-server) guide. ## Integration overview The steps below show an overview of how to accept an Apple Pay deposit. 1. **Register the user.** Call the [create-user](./apireference/user/create-user) endpoint with the user's name, email, address, and phone number. * Store the returned Push `id` alongside your internal user record. * Register each user **only once** and reuse the user's `id` on every subsequent transaction. 2. **Initialize the Apple Pay launcher.** Call the [create-user-url](./apireference/user/create-user-url) endpoint with the user's `id` and `type: "apple_pay"` when the user loads the payment page, then initialize the SDK launcher with the returned `url`. * Check `ApplePaySession.canMakePayments()` and render the Apple Pay button only when it returns `true`. For styling and placement, follow Apple's [design guidelines](https://developer.apple.com/design/human-interface-guidelines/apple-pay#Using-Apple-Pay-buttons) and [JavaScript guide](https://developer.apple.com/documentation/applepayontheweb/displaying-apple-pay-buttons-using-javascript). * To support non-Apple devices or browsers other than Safari, install the [Apple Pay JS SDK](https://developer.apple.com/documentation/applepayontheweb/loading-the-latest-version-of-apple-pay-js) on your cashier. Initiating the payment then displays a QR code the user scans with an Apple device to authorize. 3. **Display the payment sheet.** When the user clicks the Apple Pay button, call `launcher.display()` with the payment `amount`, `currency`, `direction`, and an `onAuthorize` callback. The SDK handles merchant validation and presents the payment sheet. * Optionally pass an `onComplete` callback that runs when the payment sheet is dismissed. 4. **Authorize the payment.** The `onAuthorize` callback receives a `token` once the user approves with Face ID or Touch ID. Pass the `token`, along with the same `amount`, `currency`, and `direction`, to the [authorize-payment](./apireference/authorization/authorize-payment) endpoint from your backend. * Return the resulting status (`approved` or `declined`) from `onAuthorize` to complete the Apple Pay session. The `amount` and `currency` sent to `/authorize` must match the values passed to `display()`. ```mermaid theme={null} sequenceDiagram participant User participant Operator participant SDK as Push SDK participant Apple as Apple Pay participant API as Push API Note over User,API: Step 1: Register the user Operator->>API: POST /user {name, email, address, phone} API-->>Operator: {id} Note over User,API: Step 2: Initialize User->>Operator: Load payment page Operator->>API: POST /user/{id}/url {type: "apple_pay"} API-->>Operator: {url} Operator->>SDK: new PushCash.ApplePay({url}) Operator->>Operator: Check ApplePaySession.canMakePayments() Operator->>Operator: Render Apple Pay button Note over User,API: Step 3: Display Payment Sheet User->>Operator: Click Apple Pay button Operator->>SDK: launcher.display({amount, currency, direction, onAuthorize}) SDK->>Apple: Create ApplePaySession & begin() Note over SDK,Apple: Push validates merchant via mTLS SDK->>Apple: completeMerchantValidation(session) Apple->>User: Display payment sheet Note over User,API: Step 4: Authorize Payment User->>Apple: Authorize (Face ID / Touch ID) Apple->>SDK: Receive token SDK->>Operator: onAuthorize(token) Operator->>API: POST /authorize {token, amount, currency, direction} API-->>Operator: {status: "approved" | "declined"} Operator-->>SDK: Return "approved" | "declined" SDK->>Apple: completePayment(status) ``` ```bash Register User theme={null} curl --request POST \ --url https://sandbox.pushcash.com/user \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "name": { "first": "Alfred", "last": "Hitchcock" }, "email": "alfred@imdb.com", "address": { "address_line_1": "1609 10th Ave", "locality": "Bodega Bay", "administrative_area": "CA", "postal_code": "94923", "country": "US" }, "phone_number": "(555) 681-3485", "tag": "4c8e6b4f" } ' ``` ```bash Create User URL theme={null} curl --request POST \ --url https://sandbox.pushcash.com/user/{id}/url \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "type": "apple_pay" } ' ``` ```bash Authorize Payment theme={null} curl --request POST \ --url https://sandbox.pushcash.com/authorize \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "amount": 1000, "currency": "USD", "direction": "cash_in", "token": "token_mbDRHFi3dxIZEtykHsgUGC" } ' ``` ```json Register User - 200 OK theme={null} { "id": "user_lVpbPL0K1XIiHx0DxipRbD" } ``` ```json Create User URL - 200 OK theme={null} { "url": "https://cdn.pushcash.com/applepay/?param=1¶m=2¶m=3" } ``` ```json Authorize Payment - Approved theme={null} { "id": "intent_sandbox_mbDRHFi3dxIZEtykHsgUGC", "status": "approved" } ``` ```json Authorize Payment - Declined theme={null} { "id": "intent_sandbox_mbDRHFi3dxIZEtykHsgUGC", "status": "declined" } ``` ## Launcher and payment sheet The Push SDK exposes an `ApplePay` launcher. Instantiate it from the `url` returned by [create-user-url](./apireference/user/create-user-url) **when the page loads** — not inside the button's click handler. Apple only allows the payment sheet to be presented from within a user-gesture handler, so `display()` must run synchronously on the click; the launcher it depends on has to already exist. Instantiate the launcher on page load and call `display()` directly inside the click handler. If `display()` is reached outside a user-gesture handler — for example after an `await`, or after creating the launcher on click — Apple blocks it and the payment sheet never appears. ```js theme={null} // On page load: instantiate the launcher from the create-user-url response. let applePay; try { applePay = new window.PushCash.ApplePay({ url }); submitButton.disabled = false; } catch (error) { submitButton.disabled = true; callbackResponse.textContent = `Apple Pay init error: ${error.message}`; } // On button click (a user gesture): present the payment sheet. submitButton.addEventListener('click', () => { if (!applePay) return; applePay.display({ amount: 1000, // amount of payment in cents direction: 'cash_in', currency: 'USD', onAuthorize: async (tokenId) => { // send tokenId to your backend, which calls POST /authorize const result = await yourBackend.authorize(tokenId); return result.status; // 'approved' or 'declined' }, onComplete: async () => { // reset your UI after the payment sheet is dismissed submitButton.disabled = true; }, }); }); ``` ## Domain registration Apple Pay on the web requires your cashier to be served over HTTPS on a domain that **Apple has verified**. Verification is a one-time, manual step per domain (you verify your sandbox and production domains separately), completed together with Push Cash: 1. **Supply your domain.** Give your Push Cash representative the exact domain your cashier is served from (for example, `cashier.your-domain.com`). For sandbox, email [hello@pushcash.com](mailto:hello@pushcash.com). 2. **Receive the domain association file.** Push generates and sends you the `apple-developer-merchantid-domain-association.txt` file for that domain over Slack or email. 3. **Host the file.** Place it at: ```text theme={null} https://your-domain.com/.well-known/apple-developer-merchantid-domain-association.txt ``` 4. **Push verifies the domain.** Once the file is reachable over HTTPS, Push verifies the domain in the Apple developer console. This must be done by Push — it is not self-serve. Production domain verification is completed live on your go-live call. See the [Go-Live Checklist](./go-live-checklist) for the full production cutover. ## Withdrawals A returning user can withdraw funds to a debit card they previously deposited with via Apple Pay — no re-authentication through the payment sheet is required. Users must have completed at least one deposit through the flow above before a withdrawal credential is available. Withdraw to a stored Apple Pay debit card 1. **Display stored credentials.** Call the [list-user-credentials](/apireference/user/list-user-credentials) endpoint with the user's ID, passing `apple_pay_debit` as a `type` query parameter (`?type=apple_pay_debit`). Render the returned credentials so the user can pick which card to withdraw to. * Only deposits made with a **debit** card create an `apple_pay_debit` credential. Deposits made with a credit card do not create a withdrawal-eligible credential. 2. **Submit the withdrawal.** Call the [authorize-payment](/apireference/authorization/authorize-payment) endpoint with the selected `credential_id`, `direction: cash_out`, and the `amount`. * A `200` response means the withdrawal was approved — display the result to the user. * Handle a `401` response. In a small number of cases the debit card does not support **OCT** (Original Credit Transactions), which are required to push funds to a card. This is determined by the card issuer and cannot be resolved for that card — prompt the user to select or add a different debit card. ```bash List User Credentials theme={null} curl --request GET \ --url 'https://sandbox.pushcash.com/user/{id}/credentials?type=apple_pay_debit' \ --header 'Authorization: Bearer ' ``` ```bash Authorize Withdrawal theme={null} curl --request POST \ --url https://sandbox.pushcash.com/authorize \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "user_id": "user_lVpbPL0K1XIiHx0DxipRbD", "credential_id": "cred_sandbox_123", "amount": 2500, "currency": "USD", "direction": "cash_out" } ' ``` ```json List User Credentials - 200 OK theme={null} { "data": [ { "id": "cred_7YlA9IiSl8UZvNwNSbFajV", "created_at": "2023-05-24T20:15:18.158Z", "card_last4": "4444", "authenticated": true, "type": "apple_pay_debit", "account_last4": null, "bank_name": null } ] } ``` ## Sandbox testing Test the full flow against the sandbox host (`sandbox.pushcash.com`) before going live. To exercise the payment sheet on the **web**, deploy your sandbox cashier to a verified HTTPS domain (see [Domain registration](/apple-pay-web#domain-registration)) — Apple Pay cannot run on `localhost`. In an **app**, test on a physical device signed in to an [Apple sandbox tester account](https://developer.apple.com/apple-pay/sandbox-testing/) — the iOS simulator does not produce real encrypted payment data. In sandbox, simulate a declined authorization by submitting for `2200` cents (\$22.00). Any other amount is approved. This applies to both deposit authorizations and withdrawal OCT declines. ### Simulating a stored credential To test withdrawals without performing a real Apple Pay deposit, create a stored `apple_pay_debit` credential with synthetic card data using the [simulate-credential](/apireference/simulation/simulate-a-stored-credential) endpoint: ```bash theme={null} curl --request POST \ --url https://sandbox.pushcash.com/sandbox/credential \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "user_id": "user_lVpbPL0K1XIiHx0DxipRbD", "type": "apple_pay_debit", "card_last4": "9010" } ' ``` The returned credential behaves like any stored credential: it appears in [list-user-credentials](/apireference/user/list-user-credentials) and can be used to authorize `cash_out` payments. The endpoint is available in sandbox alone. ### Integration checklist Work through the following before requesting production access: * Simulate an approved deposit by submitting a payment for \$10.00 * Simulate a declined deposit by submitting a payment for \$22.00 * Create an `apple_pay_debit` credential using the [simulate-credential](/apireference/simulation/simulate-a-stored-credential) endpoint, verify it is returned by [list-user-credentials](/apireference/user/list-user-credentials) with `?type=apple_pay_debit` and displayed with `card_last4`, and test a withdrawal to it * Test an OCT decline by submitting a `cash_out` for `2200` cents (\$22.00) against an `apple_pay_debit` credential ## Next steps * Set up webhooks to receive asynchronous updates about payment status — see the [enabling webhooks](/enabling-webhooks) guide. * Move your integration from sandbox to production with the [Go-Live Checklist](/go-live-checklist). # Card Payments Source: https://docs.pushcash.com/card-payments ## Overview This guide covers the core card payment integration end to end: registering a user, collecting their card through the Push Widget, authorizing a transaction, handling step-up authentication when the authorization engine requires it, and retrieving the final result. The widget renders and tokenizes the card, so your integration is limited to generating a URL, tokenizing through the widget, and authorizing against the Push API. Process a new card Once a user has paid, their card is stored as a reusable credential. Returning users can pay with a previously-stored credential instead of re-entering card details — see [Stored credentials](#stored-credentials). The same flow also pays users out: to process a withdrawal (`cash_out`), see [Withdrawals](#withdrawals). ## Integration overview The steps below show an overview of how to process a card payment. 1. **Register the user.** Call the [create-user](./apireference/user/create-user) endpoint with the user's identity information (name, email, address, and phone number). * Store the returned Push `id` alongside your internal user record. * Register each user **only once** and reuse the `id` on every subsequent transaction. Provide your internal user identifier in the `tag` field to ensure [idempotency](./api#idempotency). 2. **Display the widget.** Call the [create-user-url](./apireference/user/create-user-url) endpoint with the user's `id` and payment `direction`, then render the [widget](#widget) to your payment page using the returned `url`. * The widget automatically validates the card and invokes the `onValid` callback when the form is complete — enable your submit button there. * For a returning user, you can skip the widget and let them pay with a previously-stored credential instead. See [Stored credentials](#stored-credentials). 3. **Authorize the transaction.** When the user submits, generate a `token` from the widget (or use a stored `credential_id`) and call the [authorize-payment](./apireference/authorization/authorize-payment) endpoint with the payment details (`amount`, `currency`, `direction`), the `token`, and a `redirect_url`. Handle the response by its HTTP status code: * **`202 Accepted`** — the user must complete authentication. Persist the returned intent `id`, then continue to step 4 using the returned `url`. * **`200 OK`** — the payment was approved immediately. Notify the user that the payment succeeded. * **`401 Unauthorized`** — the payment was declined. If your organization is assigned **multiple settlement accounts**, include the relevant `account_id` on every authorization request — retrieve the list with [list-accounts](./apireference/ledger/list-accounts). Omitting `account_id` while multiple accounts exist will fail the request. Organizations with a single settlement account can ignore this. 4. **Direct the user to complete authentication.** Navigate the user to the `url` returned from the authorize call. When the flow completes, the user is returned to your application via the `redirect_url` you set on the authorize request. Present the authentication UI in an iframe or the device's system browser depending on your environment — see [Presenting the authentication UI](#presenting-the-authentication-ui). 5. **Retrieve the result.** Call the [get-an-intent](./apireference/intent/get-an-intent) endpoint and inspect the `status` field. * If `status` is `approved`, update your internal transaction record and notify the user the payment succeeded. * If `status` is `declined`, the payment could not be approved (e.g. `insufficient_funds`). ```bash Create User theme={null} curl --request POST \ --url https://sandbox.pushcash.com/user \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "name": { "first": "Alfred", "last": "Hitchcock" }, "email": "alfred@imdb.com", "address": { "address_line_1": "1609 10th Ave", "locality": "Bodega Bay", "administrative_area": "CA", "postal_code": "94923", "country": "US" }, "date_of_birth": "1899-08-13", "government_id": { "type": "passport", "last4": "7349" }, "phone_number": "(555) 681-3485", "tag": "4c8e6b4f", "identity_verified": true } ' ``` ```bash Create User URL theme={null} curl --request POST \ --url https://sandbox.pushcash.com/user/{id}/url \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "direction": "cash_in" } ' ``` ```bash List User Credentials theme={null} curl --request GET \ --url 'https://sandbox.pushcash.com/user/{id}/credentials?type=secure_debit&type=card_only_credit&type=card_only_debit' \ --header 'Authorization: Bearer ' ``` ```bash Authorize Payment theme={null} curl --request POST \ --url https://sandbox.pushcash.com/authorize \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "amount": 2500, "currency": "USD", "direction": "cash_in", "tag": "your-internal-transaction-record-id", "token": "token_mbDRHFi3dxIZEtykHsgUGC", "redirect_url": "https://yourapp.com/payment/complete" } ' ``` ```bash Get Intent theme={null} curl --request GET \ --url https://sandbox.pushcash.com/intent/intent_sandbox_dMggQ93ZYH6DH9LBhVeijE \ --header 'Authorization: Bearer ' ``` ```json Create User - 200 OK theme={null} { "id": "user_lVpbPL0K1XIiHx0DxipRbD" } ``` ```json Create User URL - 200 OK theme={null} { "url": "https://cdn.pushcash.com/widget/?param=1¶m=2¶m=3" } ``` ```json List User Credentials - 200 OK theme={null} { "data": [ { "id": "cred_7YlA9IiSl8UZvNwNSbFajV", "created_at": "2023-05-24T20:15:18.158Z", "card_last4": "4444", "authenticated": true, "type": "secure_debit", "account_last4": "0685", "bank_name": "Chase Bank" }, { "id": "cred_8ZlB0JjTm9VZaOxOTcGbkW", "created_at": "2023-05-25T10:20:30.123Z", "card_last4": "1234", "authenticated": true, "type": "card_only_credit", "account_last4": null, "bank_name": null } ] } ``` ```json Authorize - 200 OK (Approved) theme={null} { "id": "intent_sandbox_mbDRHFi3dxIZEtykHsgUGC", "amount": 2500, "direction": "cash_in", "currency": "USD", "credential": { "id": "cred_7YlA9IiSl8UZvNwNSbFajV", "display_name": "Chase Checking", "last4": "6018" } } ``` ```json Authorize - 202 Accepted (Authentication Required) theme={null} { "id": "intent_sandbox_dMggQ93ZYH6DH9LBhVeijE", "url": "https://cdn.pushcash.com/ux/intent_sandbox_dMggQ93ZYH6DH9LBhVeijE" } ``` ```json Authorize - 401 Unauthorized (Declined) theme={null} { "id": "intent_sandbox_mbDRHFi3dxIZEtykHsgUGC", "status": "declined", "decline_reason": "insufficient_funds" } ``` ```json Get Intent - Approved theme={null} { "id": "intent_sandbox_dMggQ93ZYH6DH9LBhVeijE", "status": "approved", "amount": 2500, "currency": "USD" } ``` ```json Get Intent - Declined theme={null} { "id": "intent_sandbox_dMggQ93ZYH6DH9LBhVeijE", "status": "declined", "decline_reason": "insufficient_funds" } ``` ## Widget The Push SDK exposes a `Widget` that renders the card form and tokenizes the card. Instantiate it with the `url` returned by [create-user-url](./apireference/user/create-user-url), rendering it into an element on your payment page. The widget validates the card as the user types and invokes `onValid` when the form is complete; enable your submit button there. ```js theme={null} const submitButton = document.querySelector('#submit-button'); submitButton.disabled = true; const widget = new window.PushCash.Widget({ // where to render the widget on the page selector: '#payment-container', // the url returned from the create-user-url endpoint url: 'https://cdn.pushcash.com/widget/?param=1¶m=2', // runs once the user provides valid card information onValid: () => { submitButton.disabled = false; }, }); ``` When the user submits, call `widget.tokenize()` to generate a token, then send it to your backend to authorize the payment. ```js theme={null} submitButton.addEventListener('click', async () => { try { const { token } = await widget.tokenize(); // send token to your backend, which calls POST /authorize await yourBackend.authorize(token); } catch (err) { console.error('Tokenization failed:', err.message); } }); ``` For full widget customization (colors, padding, typography), see the [JS SDK Reference](./sdk#widget). ## Stored credentials After a user's first payment, their card is stored as a reusable credential. A returning user can pay without re-entering card details — skip the widget and let them select a previously-stored credential instead. Process using a stored card 1. **List and display the stored credentials.** Call the [list-user-credentials](./apireference/user/list-user-credentials) endpoint with the user's `id` and the `type` values for the payment. For deposits, pass `secure_debit`, `card_only_credit`, and `card_only_debit` as the allowed types; for withdrawals, pass `secure_debit`. Display each returned credential's `card_last4` and allow the user to select which card to use. 2. **Authorize with the `credential_id`.** Call [authorize-payment](./apireference/authorization/authorize-payment) with the selected `credential_id` (instead of a widget `token`), the payment details, and a `redirect_url`. Handle the response by its HTTP status code exactly as in the [Integration overview](#integration-overview). 3. **Direct the user to complete authentication.** If authorization returns `202 Accepted`, navigate the user to the returned `url` — authentication may be requested even for a stored credential. See [Presenting the authentication UI](#presenting-the-authentication-ui). 4. **Retrieve the result.** Call the [get-an-intent](./apireference/intent/get-an-intent) endpoint and inspect `status` to confirm the payment was `approved` or `declined`. ```bash Authorize With Credential ID theme={null} curl --request POST \ --url https://sandbox.pushcash.com/authorize \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "user_id": "user_lVpbPL0K1XIiHx0DxipRbD", "credential_id": "cred_sandbox_123", "amount": 2500, "currency": "USD", "direction": "cash_in" } ' ``` ## Withdrawals The steps above collect a payment (`cash_in`). To pay a user out, follow the same five-step flow with `direction: cash_out` and the differences below. 1. **Restrict the widget to debit cards.** Withdrawals settle to a bank account, so they require a `secure_debit` credential — a debit card with a linked account. When collecting a new card for a withdrawal, pass `type: secure_debit` to [create-user-url](./apireference/user/create-user-url) so the widget only accepts debit cards. For a returning user, list stored credentials with `?type=secure_debit` before presenting the selection UI (see [Stored credentials](#stored-credentials)). 2. **Authorize with `direction: cash_out`.** Call [authorize-payment](./apireference/authorization/authorize-payment) with `direction: cash_out`. Optionally set `approval_mode: manual` to hold the approved withdrawal for review instead of posting it to the network automatically — see [Reviewing withdrawals](#reviewing-withdrawals). 3. **Direct the user to complete authentication.** Identical to a deposit — when the authorization returns `202 Accepted`, navigate the user to the returned `url`. See [Presenting the authentication UI](#presenting-the-authentication-ui). 4. **Retrieve the result.** Call [get-an-intent](./apireference/intent/get-an-intent) and inspect `status`. A withdrawal authorized with `approval_mode: manual` returns `status: pending` — Push has approved it, but it is not posted to the network until you approve it. 5. **(Optional) Approve the payment.** If you authorized with `approval_mode: manual`, approve the pending intent to post it to the network — see [Reviewing withdrawals](#reviewing-withdrawals). ```bash Create User URL (Withdrawal) theme={null} curl --request POST \ --url https://sandbox.pushcash.com/user/{id}/url \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "direction": "cash_out", "type": "secure_debit" } ' ``` ### Reviewing withdrawals Setting `approval_mode: manual` on a `cash_out` authorization holds the approved withdrawal in a `pending` state so your team can review it before funds are posted to the network. Review the intent with [get-an-intent](./apireference/intent/get-an-intent), then either [approve](./apireference/intent/approve-a-pending-intent) it to post the payment or [cancel](./apireference/intent/cancel-an-intent) it. Automatic posting can only be disabled for `cash_out` payments. Always approve or cancel manual withdrawals so every intent reaches a terminal status and is not left `pending` indefinitely. ```bash Authorize Withdrawal (Manual Approval) theme={null} curl --request POST \ --url https://sandbox.pushcash.com/authorize \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "user_id": "user_lVpbPL0K1XIiHx0DxipRbD", "amount": 1000, "currency": "USD", "direction": "cash_out", "approval_mode": "manual", "token": "token_mbDRHFi3dxIZEtykHsgUGC" } ' ``` ```bash Approve Pending Intent theme={null} curl --request POST \ --url https://sandbox.pushcash.com/intent/intent_5Jun2aRUEs7xGddsARCowB/approve \ --header 'Authorization: Bearer ' ``` ```bash Cancel Pending Intent theme={null} curl --request POST \ --url https://sandbox.pushcash.com/intent/intent_5Jun2aRUEs7xGddsARCowB/cancel \ --header 'Authorization: Bearer ' ``` ## Refunds Return funds to a user by refunding an approved `cash_in` intent. 1. **Submit the refund.** Call the [create-refund](./apireference/refund/create-a-refund) endpoint with the `intent_id` of the payment you want to refund. * Optionally include an `amount` to refund a portion of the intent. If `amount` is omitted, the full intent amount is refunded. * Only one refund is allowed per intent — subsequent refund attempts on the same intent return a `200` with the already-created refund. To check whether an intent has already been refunded, call [get-an-intent](./apireference/intent/get-an-intent) and inspect the `refund` field on the response. A refund can be declined by the issuer if the user's bank rejects the transaction. 2. **Handle the response.** Check the HTTP status code: * **`200 OK`** — the refund was created. * **`401 Unauthorized`** — the refund was declined by the payment network. The response body includes the `id` of the declined refund. * **`400 Bad Request`** — a validation error (e.g. the refund `amount` is greater than the intent amount). ```bash Create Refund (full amount) theme={null} curl --request POST \ --url https://sandbox.pushcash.com/refund \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "intent_id": "intent_R7xKpVwZ3mNqYtFh2sJcAe" } ' ``` ```bash Create Refund (partial amount) theme={null} curl --request POST \ --url https://sandbox.pushcash.com/refund \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "intent_id": "intent_R7xKpVwZ3mNqYtFh2sJcAe", "amount": 500 } ' ``` ## Disputes A **dispute** records a completed payment that the payer's bank or card issuer is attempting to reverse. When a cardholder charges back a card payment, or a receiving bank returns an ACH entry, Push opens a dispute against the original [intent](./apireference/intent/get-an-intent) so you have a single object to track the challenge and its outcome. Each dispute is tied to exactly one intent, and it also appears on the [Ledger](./ledger) as a `dispute`-type [transaction](./ledger#transactions) that reverses the original funds. Push opens a dispute from two sources, in both cases taking the disputed `amount` and a network reason `code` from the reversal: * **Card chargebacks** — a cardholder or issuer disputes a card payment. The `code` is the card-network reason code, for example `4853` (Cardholder Dispute) or `13.1` (Merchandise or Services Not Received). * **ACH returns** — the receiving bank returns an ACH entry rather than honoring it. The `code` is the ACH return code, for example `R01` (Insufficient Funds) or `R02` (Account Closed). A dispute opens in the `created` state and stays there while the challenge is worked. Card chargebacks can be contested through **representment** — Push submits evidence to the issuer on your behalf — and ACH returns may be retried. Push manages that process; the dispute stays `created` until the network reaches a decision, at which point it moves to a terminal state. ```mermaid theme={null} stateDiagram-v2 direction LR [*] --> created: chargeback or ACH return received created --> won: resolved in your favor created --> lost: resolved against you won --> [*] lost --> [*] ``` | Status | Meaning | | :-------- | :----------------------------------------------------------------------------------------------------------- | | `created` | The dispute is open. The disputed funds are provisionally reversed while the chargeback or return is worked. | | `won` | Resolved in your favor. The reversal did not stand and the funds remain yours. | | `lost` | Resolved against you. The disputed funds are reclaimed. | Track disputes through both a push and a pull path: * **`dispute.created` webhook (push).** When a dispute is opened against one of your intents, Push sends a signed [`dispute.created`](./enabling-webhooks#webhook-types) event to the webhook endpoint you configured on the originating payment — the signal to listen for so you learn about a chargeback or return without polling. * **Dispute API (pull).** Fetch a single dispute with [get-a-dispute](./apireference/dispute/retrieve-a-dispute), or query them with [list-disputes](./apireference/dispute/list-disputes) filtering by `status`, `code`, `type`, and creation time. The current dispute is also embedded as the `dispute` field on the [get-an-intent](./apireference/intent/get-an-intent) response. ## Presenting the authentication UI When [authorize-payment](./apireference/authorization/authorize-payment) returns `202 Accepted`, present the authentication UI hosted at the returned `url`. Choose the presentation based on how the user accesses your application: User authentication * **Web (mobile or desktop):** use the [Push JS SDK](./sdk#ux)'s `UX` to present the Push-hosted UI in a modal, and omit `redirect_url` from the authorization request. It fires your `onExit` callback when the flow ends. * **Native mobile app:** open the authentication URL in the device's default system browser (Safari on iOS, Chrome on Android). Set `redirect_url` on the authorization request to bring the user back to your app when authentication completes. Use a custom scheme (e.g. `pushcash://`) or a Universal Link / App Link as your `redirect_url` so the system browser can hand control back to your app. On the web, pass the intent `id` from the `202` response to `UX` and open the modal. `onExit` fires once when the flow ends — the user either completes or dismisses it — at which point you confirm the outcome with [get-an-intent](./apireference/intent/get-an-intent): ```js theme={null} const ux = window.PushCash.UX({ // the intent id returned from the authorize 202 response intentId: 'intent_sandbox_dMggQ93ZYH6DH9LBhVeijE', onExit: async () => { // the flow has ended — confirm the result from your backend (GET /intent/{id}) const intent = await yourBackend.getIntent('intent_sandbox_dMggQ93ZYH6DH9LBhVeijE'); // update your UI based on intent.status ('approved' | 'declined' | ...) }, }); ux.open(); ``` ```mermaid theme={null} flowchart TD A[User needs to complete step-up authentication] --> B{Is user accessing the payment page via a standard browser environment which supports opening additional windows / browser tabs?} B -- Yes --> C[**Use the SDK**
- Omit redirect_url from the authorization request
- Present the UI with the Push JS SDK's UX, which detects completion for you] B -- No --> D{Is the user accessing the payment page via a
native mobile app?} D -- Yes --> E[**Use redirect**
- Set redirect_url parameter in authorization request
- Open the authentication URL in the device's default system browser
] D -- No --> F[Default to the SDK's UX
Omit redirect_url] ``` ## Sandbox testing Work through the following against the sandbox host (`sandbox.pushcash.com`) before requesting production access: * Register each user only once and reuse their `id` on subsequent transactions * Simulate an approved transaction using test card `5555 5555 5555 4444` * Simulate a declined transaction using test card `5999 9919 6976 9266` * Test an authentication-required (`202 Accepted`) response using test card `6011 0009 9013 9424`, and confirm you persist the intent `id` and present the authentication UI at `url` * Test a payment decline after the user completes authentication using test card `5999 9819 6976 9283` * Add a new card, then submit a second transaction using the stored `credential_id` * Verify that only `secure_debit` credentials are presented for withdrawals * Process a withdrawal (`cash_out`) with a `secure_debit` card, then authorize with `approval_mode: manual` and approve the pending intent * Refund an approved intent, and test a partial refund by specifying an `amount` less than the original intent amount * Test a refund decline by processing an intent with test card `5999 9619 6976 9301` and then submitting a refund ## Next steps Now that you can process card payments, set up webhooks to receive asynchronous updates on the final payment result — see the [enabling webhooks](./enabling-webhooks) guide. # Changelog Source: https://docs.pushcash.com/changelog Product updates and announcements ## API Updates * New [Ledger](/ledger) reference guide covering transactions, transfers, and reconciliation. * The transaction and transfer objects now use the settlement-lifecycle model: unsigned `amount`, `cash_in` / `cash_out` direction, and a four-stage transaction `status`. * New [Disputes](/card-payments#disputes) section in the Card Payments guide and a `dispute.created` [webhook](/enabling-webhooks#webhook-types) delivered when a chargeback or ACH return is opened against a payment. ## Details The [Ledger guide](/ledger) explains how Push records money movement as transactions and settles them into transfers on a fixed schedule, and how to reconcile that activity against your own books. It consolidates the former Settlement reference into a single guide. The [Disputes](/card-payments#disputes) section of the Card Payments guide explains how Push records card chargebacks and ACH returns as disputes tied to the original intent, how a dispute resolves to `won` or `lost`, and how to track it. Push now delivers a [`dispute.created`](/enabling-webhooks#webhook-types) webhook to the endpoint configured on the originating payment when a dispute is opened, so you learn a payment is being reversed without polling. The `transaction` object has been updated to reflect the settlement lifecycle directly: * `amount` is now an unsigned integer; use `direction` (`cash_in` / `cash_out`) to determine which way the money moved, replacing the previous signed amount with `credit` / `debit`. * `status` now exposes the full lifecycle — `pending`, `available` (cash\_in only), `in_transfer` (cash\_out only), and `settled` — rather than only `pending` / `settled`. * `type` is one of `intent`, `refund`, or `dispute`, identifying what created the transaction. * A `transfer` object is now included once a transaction reaches `in_transfer` or `settled`, linking it to the transfer that settles it. * The `batch` field has been removed. The `transfer` object now reports an unsigned `amount` and a `direction` of `disbursement` (a net payout to your operational account) or `collection` (a net withdrawal from it), and its `transactions` array is the exact set of transactions that net to the transfer amount. See [Reconciliation](/ledger#reconciliation) for how to use this. ## API Updates * Reorganized the documentation site around a focused set of core integration guides. * Published the Apple Pay withdrawals flow. * Published the ACH Save integration guide. ## Details The documentation site has been reorganized around a focused set of core integration guides — [Card Payments](/card-payments), [Apple Pay](/apple-pay), [ACH Save](/ach-save), and [Webhooks](/enabling-webhooks). Supplementary topics such as stored credentials, withdrawals, refunds, manual review, and multiple settlement accounts are now folded into the guides they belong to, and webhook event types are documented alongside the [Webhooks guide](/enabling-webhooks#webhook-types). See the [welcome guide](/welcome-guide) for an overview. Apple Pay now supports withdrawals (`cash_out`). A returning user can withdraw to a debit card they previously deposited with — no separate account linking required. See [Withdrawals](/apple-pay-web#withdrawals) in the Apple Pay guide. ACH Save is now documented. ACH Save recovers eligible declined card transactions by retrying them over ACH through an authenticated bank flow, lifting authorization rates on otherwise-lost volume. See the [ACH Save guide](/ach-save). ## API Updates * Refunds are now supported via the API, including issuing and managing refunds. * Simplified Apple Pay integration flow * Increased rate limits ## Details Operators can now issue refunds and track their status directly through the API. Use [create-refund](/apireference/refund/create-a-refund) to issue a refund, [get-a-refund](/apireference/refund/get-a-refund) to retrieve details for an individual refund, and [list-refunds](/apireference/refund/list-refunds) to query all refunds. The Apple Pay integration has been simplified — operators now call [authorize-payment](/apireference/authorization/authorize-payment) directly with the Apple Pay token, removing the need for a separate tokenization step. See the updated [Apple Pay guide](/apple-pay) for details. The [API Concepts](/api) page now includes an [environments section](/api#environments) documenting sandbox and production base URLs. [Rate limits](/api#rate-limiting) have been increased and the window has been shortened to per-minute, giving operators higher throughput for bursty workloads. ## API Updates * New `brand` field returned on card objects in intent and user responses. ## Details A `brand` field is now included on card objects returned by [get-intent](/apireference/intent/get-an-intent), [list-intents](/apireference/intent/list-intents), [get-user](/apireference/user/get-a-user), and [list-users](/apireference/user/list-users) endpoints. The field returns one of `mastercard`, `visa`, `amex`, or `discover`, enabling operators to display card brand information without maintaining their own BIN lookup. ## API Updates * New integration guides and restructured documentation. * Support for dynamic type assignment by omitting the type parameter in requests to the API * New `tag` field on the [authorize-payment](/apireference/authorization/authorize-payment) endpoint. ## Details The documentation has been completely refreshed with new step-by-step integration guides covering the full payment lifecycle. The previous hosted payment experience, card-only, and legacy integration guides have been retired and replaced with focused guides for each core workflow. See the [welcome guide](/welcome-guide) for an overview. The `tag` field on [authorize-payment](/apireference/authorization/authorize-payment) allows operators to supply an internal transaction record ID alongside the payment request for easier reconciliation. The `type` field on the [create-user-url](/apireference/user/create-user-url) request body is now optional. Operators who wish to have the payment type determined dynamically from the card BIN and operator configuration can omit the parameter. ## API Updates * Deprecation of idempotency header for create-user and authorize-payment endpoints. ## Details `X-Idempotency-Key` is now deprecated for the [create-user](/apireference/user/create-user#body-tag) and [authorize-payment](/apireference/authorization/authorize-payment) endpoints. Developers can continue to submit requests with the parameter, but it will no longer be read or used in the de-duplication of requests. For more details on how our API idempotent request handling logic works, see our [API Concepts](/api#idempotency) guide. # Webhooks Source: https://docs.pushcash.com/enabling-webhooks ## Overview Webhooks deliver asynchronous notifications about the final result of a payment, so your internal transaction state stays accurate even if the authorization request times out or returns an ambiguous response. When an intent reaches a terminal state, Push sends a signed event — [`intent.approved`](#webhook-types) or [`intent.declined`](#webhook-types) — to an HTTPS endpoint you control. Push also emits ledger events as funds move through settlement. When a transaction reaches its terminal `settled` state, Push sends a [`transaction.settled`](#webhook-types) event so you can confirm that funds have settled without polling. See the [Ledger guide](./ledger) for the transaction lifecycle. Push emits a [`dispute.created`](#webhook-types) event when a completed payment is challenged — a card chargeback or an ACH return — so you learn a payment is being reversed without polling. See [Disputes](./card-payments#disputes) for the dispute lifecycle. Events are delivered at-least-once and may arrive in any order relative to the authorization response, so your endpoint must verify each event, process it idempotently, and tolerate out-of-order delivery. ## Integration overview The steps below show an overview of how to receive and process webhooks. 1. **Configure webhook delivery.** When calling [authorize-payment](./apireference/authorization/authorize-payment), provide: * `webhook_url` — where Push delivers webhook events. * `webhook_secret` — used to sign each event so you can verify its authenticity (32 characters minimum). * `tag` — maps each event back to your internal transaction record. 2. **Expose an HTTPS endpoint.** Create an endpoint at your `webhook_url` that accepts POST requests and is reachable over HTTPS from the [Push IP addresses](#security). 3. **Verify each event.** Verify the request signature and timestamp before processing, and reject invalid requests with `401 Unauthorized`. See [Security](#security). 4. **Update your record and acknowledge.** Parse the payload (see [Webhook types](#webhook-types)), update the matching transaction record (keyed on the `tag` or intent `id`), and return `200 OK`. Your integration must tolerate out-of-order and duplicate deliveries — see [Independent ordering](#independent-ordering) and [Idempotency](#idempotency). ```bash Authorize Payment with Webhook Configuration theme={null} curl --request POST \ --url https://sandbox.pushcash.com/authorize \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data ' { "user_id": "user_lVpbPL0K1XIiHx0DxipRbD", "amount": 2500, "currency": "USD", "direction": "cash_in", "token": "token_mbDRHFi3dxIZEtykHsgUGC", "webhook_url": "https://yourapp.com/webhooks/push", "webhook_secret": "whsec_32_characters_minimum", "tag": "txn_12345" } ' ``` ```json intent.approved theme={null} { "type": "intent.approved", "data": { "id": "intent_sandbox_mbDRHFi3dxIZEtykHsgUGC", "tag": "txn_12345", "direction": "cash_in", "amount": 5000, "status": "approved", "rail": "ach", "currency": "USD", "type": "secure_debit", "limits_utilization": { "daily_cash_in": "5000.00", "daily_cash_out": "0.00", "monthly_cash_in": "5000.00", "monthly_cash_out": "0.00" } }, "timestamp": "2024-01-14T22:31:02.756096Z" } ``` ```json intent.declined theme={null} { "type": "intent.declined", "data": { "id": "intent_sandbox_mbDRHFi3dxIZEtykHsgUGC", "tag": "txn_12345", "direction": "cash_in", "amount": 5000, "status": "declined", "decline_category": "insufficient_funds", "currency": "USD", "type": "card_only_debit" }, "timestamp": "2024-01-14T22:31:02.756096Z" } ``` ```json transaction.settled theme={null} { "type": "transaction.settled", "data": { "id": "txn_CpiSd1bptYB5P55ysTDHg", "amount": 5000, "direction": "cash_in", "currency": "USD", "status": "settled", "type": "intent", "source_id": "intent_sandbox_mbDRHFi3dxIZEtykHsgUGC", "account_id": "account_WsELzpJOvU6fNafvzWbF6K", "created_at": "2024-01-13T20:15:18.158Z", "date": "2024-01-13", "transfer": { "id": "transfer_xle830ef8djeoiu", "amount": 700284, "currency": "USD", "direction": "disbursement", "created_at": "2024-01-14T21:00:00.000Z", "date": "2024-01-14" } }, "timestamp": "2024-01-14T22:31:02.756096Z" } ``` ```json dispute.created theme={null} { "type": "dispute.created", "data": { "id": "dispute_0293cj9ru032lisdjow", "amount": 5000, "currency": "USD", "status": "created", "intent_id": "intent_sandbox_mbDRHFi3dxIZEtykHsgUGC", "code": "R01", "type": "secure_debit", "created_at": "2024-01-14T22:31:02.756096Z" }, "timestamp": "2024-01-14T22:31:02.756096Z" } ``` ## Webhook types Push sends the following events. Each event has a `type`, a `data` object with the fields below, and a `timestamp` (ISO 8601). ### `intent.approved` Triggered when an intent is successfully approved. | Field | Type | Description | | :------------------------------------ | :------ | :----------------------------------------------------------------------------------- | | `id` | string | Unique identifier for the intent | | `tag` | string | Your internal transaction identifier (if provided) | | `direction` | string | Either cash\_in or cash\_out | | `amount` | integer | Amount in cents (e.g. 5000 = \$50.00) | | `status` | string | Intent status (`approved` or `pending`) | | `rail` | string | Payment rail used (`ach` or `card`) | | `currency` | string | Currency code (USD) | | `type` | string | Payment type (`secure_debit`, `card_only_credit`, `card_only_debit`, or `apple_pay`) | | `limits_utilization` | object | Object containing limit utilization details | | `limits_utilization.daily_cash_in` | string | Daily cash-in limit usage | | `limits_utilization.daily_cash_out` | string | Daily cash-out limit usage | | `limits_utilization.monthly_cash_in` | string | Monthly cash-in limit usage | | `limits_utilization.monthly_cash_out` | string | Monthly cash-out limit usage | ### `intent.declined` Triggered when an intent is declined. | Field | Type | Description | | :----------------- | :------ | :--------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | Unique identifier for the intent | | `tag` | string | Your internal transaction identifier (if provided) | | `direction` | string | Either `cash_in` or `cash_out` | | `amount` | integer | Amount in cents (e.g. 5000 = \$50.00) | | `status` | string | Intent status (`declined`) | | `decline_category` | string | Reason for decline – see the [full list of decline categories](./apireference/authorization/authorize-payment#response-decline-category) | | `currency` | string | Currency code (USD) | | `type` | string | Payment type (`secure_debit`, `card_only_credit`, `card_only_debit`, or `apple_pay`) | ### `transaction.settled` Triggered when a transaction reaches its terminal `settled` status — the funds have settled with the network. For `cash_in` the funds have been transmitted; for `cash_out` the network has confirmed the counterparty received the funds. The event is delivered to the same endpoint you configured for the originating payment. The `data` object is the settled [transaction](./ledger#transactions), including the [transfer](./ledger#transfers) that settled it. | Field | Type | Description | | :-------------------- | :------ | :--------------------------------------------------------------------------- | | `id` | string | Unique identifier for the transaction (prefixed `txn_`) | | `amount` | integer | Unsigned amount in cents (e.g. 5000 = \$50.00). Use `direction` for the sign | | `direction` | string | Either `cash_in` or `cash_out` | | `currency` | string | Currency code (USD) | | `status` | string | Always `settled` for this event | | `type` | string | What created the transaction: `intent`, `refund`, or `dispute` | | `source_id` | string | The ID of the intent, refund, or dispute that created the transaction | | `account_id` | string | The account the transaction settled against | | `created_at` | string | When the transaction was recorded (ISO 8601) | | `date` | string | The settlement window in which the transaction was submitted | | `transfer` | object | The transfer that settled this transaction | | `transfer.id` | string | Unique identifier for the transfer (prefixed `transfer_`) | | `transfer.amount` | integer | Net transfer amount in cents | | `transfer.currency` | string | Currency code (USD) | | `transfer.direction` | string | `disbursement` (net payout to you) or `collection` (net withdrawal from you) | | `transfer.date` | string | The settlement date of the transfer | | `transfer.created_at` | string | When the transfer was created (ISO 8601) | To reconcile the event against your own records, match `source_id` (or the `tag` you set on the originating payment) to your internal record, and use the `transfer` object to identify the settlement it landed in. See the [Ledger guide](./ledger#reconciliation). ### `dispute.created` Triggered when a dispute is opened against one of your payments — a card chargeback or an ACH return reversing a completed `cash_in`. The event is delivered to the same endpoint you configured for the originating payment. The `data` object is the [dispute](./card-payments#disputes). A dispute opens in the `created` status; its resolution to `won` or `lost` is **not** delivered as a webhook — refetch the dispute or intent to observe the outcome. See [Disputes](./card-payments#disputes). | Field | Type | Description | | :----------- | :------ | :--------------------------------------------------------------------------------------------------------------- | | `id` | string | Unique identifier for the dispute (prefixed `dispute_`) | | `amount` | integer | Disputed amount in cents (e.g. 5000 = \$50.00) | | `currency` | string | Currency code (USD) | | `status` | string | Always `created` for this event | | `intent_id` | string | The intent the dispute was opened against | | `code` | string | The network reason code — an ACH return code (e.g. `R01`) or a card-network chargeback reason code (e.g. `4853`) | | `type` | string | The payment flow type of the associated intent — either `card_only` or `secure_debit`; never `push` | | `created_at` | string | When the dispute was opened (ISO 8601) | To reconcile the event against your own records, match `intent_id` (or the `tag` you set on the originating payment) to your internal record. See [Disputes](./card-payments#disputes). ## Independent ordering Webhook delivery and authorization responses are independent and may arrive in any order. Your system must handle both scenarios by ensuring that the internal transaction record is committed to the database *before* the call to the authorization endpoint: 1. Webhook arrives first (before authorization response) 2. Authorization response arrives first (before webhook) *Sequence diagram illustrates the two possible scenarios of ordering between webhook delivery and authorization results.* Sequence diagram illustrates the two possible scenarios of ordering between webhook delivery and authorization results. ## Idempotency Push provides at-least-once delivery for webhooks. Your application must handle duplicate webhook deliveries gracefully using database transactions to ensure idempotency. In the case of a duplicated webhook delivery from Push either due to an error or timeout from your callback handler, you should discard the request and return a `200 OK`. If Push does not receive a `200 OK` response from your webhook endpoint, delivery will be retried with exponential backoff. | Environment | Retry Behavior | | :------------- | :---------------------------------------------------- | | **Sandbox** | Up to 3 attempts | | **Production** | Up to 40 attempts over 3 days, then marked as expired | ## Security Webhooks deliver data directly to an endpoint you control over the public internet. Because they are invoked automatically by Push, webhook endpoints **must be explicitly secured** to prevent unauthorized requests, data tampering, and replay attacks. Without proper verification, a malicious actor could spoof webhook requests and falsely mark payments as approved or declined in your system. If your cloud data environment restricts network access from external IPs via a firewall, you may need to allow inbound traffic from Push IP addresses in order to receive webhook requests. **Production** `44.238.180.175` **Sandbox** `34.209.246.44` ### Signature verification When a `webhook_secret` is provided, Push signs each webhook request using an HMAC-SHA256 signature derived from the raw request payload. This allows your application to verify that: * The request was sent by Push * The payload has not been modified in transit Every webhook request must be verified before it is processed. **Signature format** ``` X-Webhook-Signature: sha256= ``` **Verification steps** 1. Extract the signature from the `X-Webhook-Signature` header 2. Read the raw request body as bytes (before parsing JSON) 3. Compute an HMAC-SHA256 signature using your `webhook_secret` and the raw body 4. Compare the computed signature to the received signature using constant-time comparison 5. Reject requests with invalid signatures (`401 Unauthorized`) ```javascript JavaScript (Node.js) theme={null} const crypto = require('crypto'); function verifyWebhookSignature(secret, rawBody, signatureHeader) { // Parse signature from header if (!signatureHeader || !signatureHeader.startsWith('sha256=')) { return false; } const receivedSignature = signatureHeader.substring(7); // Remove 'sha256=' prefix // Compute expected signature const hmac = crypto.createHmac('sha256', secret); hmac.update(rawBody); // rawBody must be Buffer or string const expectedSignature = hmac.digest('hex'); // Constant-time comparison to prevent timing attacks return crypto.timingSafeEqual( Buffer.from(receivedSignature, 'hex'), Buffer.from(expectedSignature, 'hex') ); } // Express.js example app.post('/webhooks/push', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-webhook-signature']; const rawBody = req.body; // Raw buffer from express.raw() if (!verifyWebhookSignature(WEBHOOK_SECRET, rawBody, signature)) { return res.status(401).send('Invalid signature'); } // Parse JSON after verification const payload = JSON.parse(rawBody.toString()); // Verify timestamp (see below) // Process webhook... res.status(200).send('OK'); }); ``` ```python Python theme={null} import hmac import hashlib import time from datetime import datetime, timezone def verify_webhook_signature(secret: str, raw_body: bytes, signature_header: str) -> bool: """Verify webhook signature using constant-time comparison.""" if not signature_header or not signature_header.startswith('sha256='): return False received_signature = signature_header[7:] # Remove 'sha256=' prefix # Compute expected signature expected_signature = hmac.new( secret.encode('utf-8'), raw_body, hashlib.sha256 ).hexdigest() # Constant-time comparison return hmac.compare_digest(received_signature, expected_signature) # Flask example @app.route('/webhooks/push', methods=['POST']) def handle_webhook(): signature = request.headers.get('X-Webhook-Signature') raw_body = request.get_data() if not verify_webhook_signature(WEBHOOK_SECRET, raw_body, signature): return 'Invalid signature', 401 payload = request.get_json() # Verify timestamp (see below) # Process webhook... return 'OK', 200 ``` ```go Go theme={null} package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "io" "net/http" "strings" ) func verifyWebhookSignature(secret string, rawBody []byte, signatureHeader string) bool { if !strings.HasPrefix(signatureHeader, "sha256=") { return false } receivedSignature := signatureHeader[7:] // Remove 'sha256=' prefix // Compute expected signature mac := hmac.New(sha256.New, []byte(secret)) mac.Write(rawBody) expectedSignature := hex.EncodeToString(mac.Sum(nil)) // Constant-time comparison return hmac.Equal([]byte(receivedSignature), []byte(expectedSignature)) } func handleWebhook(w http.ResponseWriter, r *http.Request) { signature := r.Header.Get("X-Webhook-Signature") rawBody, _ := io.ReadAll(r.Body) if !verifyWebhookSignature(webhookSecret, rawBody, signature) { http.Error(w, "Invalid signature", http.StatusUnauthorized) return } // Parse JSON after verification var payload WebhookPayload json.Unmarshal(rawBody, &payload) // Verify timestamp (see below) // Process webhook... w.WriteHeader(http.StatusOK) } ``` ```csharp C# (.NET) theme={null} using System; using System.Security.Cryptography; using System.Text; using Microsoft.AspNetCore.Mvc; public class WebhookSignatureVerifier { public static bool VerifyWebhookSignature(string secret, byte[] rawBody, string signatureHeader) { // Parse signature from header if (string.IsNullOrEmpty(signatureHeader) || !signatureHeader.StartsWith("sha256=")) { return false; } string receivedSignature = signatureHeader.Substring(7); // Remove 'sha256=' prefix // Compute expected signature using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret))) { byte[] hashBytes = hmac.ComputeHash(rawBody); string expectedSignature = BitConverter.ToString(hashBytes) .Replace("-", "") .ToLower(); // Constant-time comparison to prevent timing attacks return CryptographicOperations.FixedTimeEquals( Encoding.UTF8.GetBytes(receivedSignature), Encoding.UTF8.GetBytes(expectedSignature) ); } } } // ASP.NET Core Controller Example [ApiController] [Route("webhooks")] public class WebhooksController : ControllerBase { private readonly string _webhookSecret; public WebhooksController(IConfiguration configuration) { _webhookSecret = configuration["WebhookSecret"]; } [HttpPost("push")] public async Task HandleWebhook() { string signature = Request.Headers["X-Webhook-Signature"]; // Read raw body as bytes using (var reader = new StreamReader(Request.Body, Encoding.UTF8, leaveOpen: true)) { Request.Body.Position = 0; byte[] rawBody = await new StreamReader(Request.Body).BaseStream.ReadAllBytesAsync(); if (!WebhookSignatureVerifier.VerifyWebhookSignature(_webhookSecret, rawBody, signature)) { return Unauthorized(new { error = "Invalid signature" }); } // Parse JSON after verification string bodyString = Encoding.UTF8.GetString(rawBody); var payload = JsonSerializer.Deserialize(bodyString); // Verify timestamp (see below) // Process webhook... return Ok(); } } } ``` ```php PHP theme={null} 'Invalid signature']); exit; } // Parse JSON after verification $payload = json_decode($rawBody, true); // Verify timestamp (see below) // Process webhook... http_response_code(200); echo json_encode(['status' => 'OK']); ?> // Laravel Example Route::post('/webhooks/push', function (Request $request) { $signature = $request->header('X-Webhook-Signature'); $rawBody = $request->getContent(); // Get raw request body if (!verifyWebhookSignature(env('WEBHOOK_SECRET'), $rawBody, $signature)) { return response()->json(['error' => 'Invalid signature'], 401); } $payload = json_decode($rawBody, true); // Verify timestamp (see below) // Process webhook... return response()->json(['status' => 'OK'], 200); }); // Symfony Example use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; class WebhookController extends AbstractController { #[Route('/webhooks/push', methods: ['POST'])] public function handleWebhook(Request $request): Response { $signature = $request->headers->get('X-Webhook-Signature'); $rawBody = $request->getContent(); if (!$this->verifyWebhookSignature($_ENV['WEBHOOK_SECRET'], $rawBody, $signature)) { return new Response( json_encode(['error' => 'Invalid signature']), Response::HTTP_UNAUTHORIZED, ['Content-Type' => 'application/json'] ); } $payload = json_decode($rawBody, true); // Verify timestamp (see below) // Process webhook... return new Response( json_encode(['status' => 'OK']), Response::HTTP_OK, ['Content-Type' => 'application/json'] ); } } ``` ```ruby Ruby on Rails theme={null} require 'openssl' def verify_webhook_signature(secret, raw_body, signature_header) # Parse signature from header return false if signature_header.nil? || !signature_header.start_with?('sha256=') received_signature = signature_header[7..-1] # Remove 'sha256=' prefix # Compute expected signature expected_signature = OpenSSL::HMAC.hexdigest('SHA256', secret, raw_body) # Constant-time comparison to prevent timing attacks ActiveSupport::SecurityUtils.secure_compare(received_signature, expected_signature) end # Rails Controller Example class WebhooksController < ApplicationController # Disable CSRF protection for webhook endpoint skip_before_action :verify_authenticity_token def push signature = request.headers['X-Webhook-Signature'] raw_body = request.raw_post # Get raw request body unless verify_webhook_signature(WEBHOOK_SECRET, raw_body, signature) render json: { error: 'Invalid signature' }, status: :unauthorized return end payload = JSON.parse(raw_body) # Verify timestamp (see below) # Process webhook... head :ok end end ``` ### Timestamp Verification The `timestamp` field in the webhook payload indicates when the webhook was created. To prevent replay attacks, verify that the timestamp is recent (within 10 minutes). **Verification Steps** 1. Parse the `timestamp` field from the payload (`ISO 8601`format) 2. Compare with current time 3. Reject requests older than 10 minutes (return`401 Unauthorized`) ```javascript JavaScript (Node.js) theme={null} function verifyWebhookTimestamp(timestamp, maxAgeMinutes = 10) { const webhookTime = new Date(timestamp); const now = new Date(); const ageMinutes = (now - webhookTime) / 1000 / 60; return ageMinutes <= maxAgeMinutes; } // In your webhook handler if (!verifyWebhookTimestamp(payload.timestamp)) { return res.status(401).send('Webhook timestamp too old'); } ``` ```python Python theme={null} from datetime import datetime, timezone, timedelta def verify_webhook_timestamp(timestamp_str: str, max_age_minutes: int = 10) -> bool: """Verify webhook timestamp is within acceptable age.""" webhook_time = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00')) now = datetime.now(timezone.utc) age = now - webhook_time return age <= timedelta(minutes=max_age_minutes) # In your webhook handler if not verify_webhook_timestamp(payload['timestamp']): return 'Webhook timestamp too old', 401 ``` ```go Go theme={null} import "time" func verifyWebhookTimestamp(timestamp string, maxAgeMinutes int) bool { webhookTime, err := time.Parse(time.RFC3339, timestamp) if err != nil { return false } age := time.Since(webhookTime) return age <= time.Duration(maxAgeMinutes) * time.Minute } // In your webhook handler if !verifyWebhookTimestamp(payload.Timestamp, 10) { http.Error(w, "Webhook timestamp too old", http.StatusUnauthorized) return } ``` ```csharp C# (.NET) theme={null} using System; public class WebhookTimestampVerifier { public static bool VerifyWebhookTimestamp(string timestampStr, int maxAgeMinutes = 10) { try { // Parse ISO 8601 timestamp DateTime webhookTime = DateTime.Parse(timestampStr, null, System.Globalization.DateTimeStyles.RoundtripKind); // Ensure UTC if (webhookTime.Kind != DateTimeKind.Utc) { webhookTime = webhookTime.ToUniversalTime(); } DateTime now = DateTime.UtcNow; TimeSpan age = now - webhookTime; // Check if timestamp is in the future (clock skew protection) if (age.TotalMinutes < 0) { return false; } return age.TotalMinutes <= maxAgeMinutes; } catch (FormatException) { // Invalid timestamp format return false; } } } // In your webhook handler if (!WebhookTimestampVerifier.VerifyWebhookTimestamp(payload.Timestamp)) { return Unauthorized(new { error = "Webhook timestamp too old" }); } ``` ```php PHP theme={null} diff($webhookTime); $ageMinutes = ($interval->days * 24 * 60) + ($interval->h * 60) + $interval->i + ($interval->s / 60); // Check if webhook is from the future (clock skew protection) if ($interval->invert === 0) { return false; } return $ageMinutes <= $maxAgeMinutes; } catch (Exception $e) { // Invalid timestamp format return false; } } // In your webhook handler if (!verifyWebhookTimestamp($payload['timestamp'])) { http_response_code(401); echo json_encode(['error' => 'Webhook timestamp too old']); exit; } // In your webhook handler (Laravel) if (!verifyWebhookTimestamp($payload['timestamp'])) { return response()->json(['error' => 'Webhook timestamp too old'], 401); } // In your webhook handler (Symfony) if (!$this->verifyWebhookTimestamp($payload['timestamp'])) { return new JsonResponse( ['error' => 'Webhook timestamp too old'], Response::HTTP_UNAUTHORIZED ); } ``` ```ruby Ruby on Rails theme={null} require 'time' def verify_webhook_timestamp(timestamp_str, max_age_minutes = 10) # Parse ISO 8601 timestamp webhook_time = Time.parse(timestamp_str).utc now = Time.now.utc age_minutes = (now - webhook_time) / 60.0 age_minutes <= max_age_minutes rescue ArgumentError # Invalid timestamp format false end # In your webhook handler unless verify_webhook_timestamp(payload['timestamp']) render json: { error: 'Webhook timestamp too old' }, status: :unauthorized return end ``` # Go-Live Checklist Source: https://docs.pushcash.com/go-live-checklist # Overview This checklist covers everything required to move your integration from sandbox to production. It is designed to be worked through **live, on a call** with your Push Cash representative, so that any missing information can be supplied in the moment and any blockers resolved before you go live. Please have an engineer with access to your production backend and production cashier domain present. By the time the call wraps, you will have: 1. **Production API key access + IP address restriction** for the production API (`api.pushcash.com`) 2. **Account identifiers** for the accounts you will transact against 3. A **configured & verified Apple Pay domain** for your production cashier (Apple Pay Web integrations only) Throughout this guide, production requests are made to `api.pushcash.com`. The same API surface is available in sandbox at `sandbox.pushcash.com` for testing beforehand. ## Step 1: Production API key & IP allowlisting Production keys are scoped to your organization and follow the format `pcsk_{env}_{public_id}.{private_key}`. Confirm the `production` segment is present — for example: ```text theme={null} pcsk_production_ct0hVCfF6C.FDlfQgAUpcfiRxBN2JvT9TgkFQri6d9XpJfzX1mu8RqD ``` Verify the key is valid and resolves to the correct organization by calling `POST /keys/verify`. The API responds with `200` (OK) and your organization name. ```bash theme={null} export APIKEY=pcsk_production_... curl --request POST \ --url https://api.pushcash.com/keys/verify \ --header 'Authorization: Bearer '$APIKEY ``` ```text theme={null} Hello Sportsbook ``` ### IP address restriction As an additional layer of security, Push Cash can restrict **write requests** (`POST`, `PUT`, `PATCH`, `DELETE`) so they are only accepted from a set of source IP addresses that you specify. This protects your account even if an API key is leaked, since a stolen key cannot move money from an unrecognized network. Read requests (`GET`, `HEAD`) are not restricted, so dashboards, monitoring, and reconciliation jobs are unaffected. IP allowlisting is configured by Push Cash — it is not self-serve. During the call, provide the **stable egress IPs** of your production backend (the public IPs of your NAT gateways or load balancers, not individual servers that may change). Include **every** IP your traffic can egress from to avoid intermittent `403` rejections. Notify Push Cash **before** changing your network egress (for example, migrating regions or adding a new gateway) so your allowlist can be updated ahead of the cutover. For more detail, see the [IP Allowlisting](/api#ip-allowlisting) section of the API Concepts guide. ## Step 2: Account identifiers Account IDs follow the format `account_WsELzpJOvU6fNafvzWbF6K` and are submitted as the `account` parameter to [`POST /authorize`](/apireference/authorization/authorize-payment). Confirm the production account ID(s) you will transact against, then verify each one by calling `GET /account/{id}` and checking the `name` in the response. ```bash theme={null} curl --request GET \ --url https://api.pushcash.com/account/account_7lnfBi4mBOWjNqW5J8Mx9E \ --header 'Authorization: Bearer '$APIKEY ``` ```json theme={null} { "id": "account_7lnfBi4mBOWjNqW5J8Mx9E", "type": "settlement", "name": "Sportsbook NJ", "created_at": "2026-06-05T00:48:56.827669Z" } ``` You can retrieve all accounts configured for your organization at any time by calling [`GET /accounts/list`](/apireference/ledger/list-accounts). ## Step 3: Apple Pay domain verification This step applies only to integrations using Apple Pay Web. For full details, see the [Apple Pay Web](/apple-pay-web) guide. Apple Pay on the web requires your production cashier to be served over HTTPS on a domain that **Apple has verified**. Verification is a manual, one-time step completed together on the call: 1. **Supply your production domain.** Give your Push Cash representative the exact domain your cashier will be served from (for example, `cashier.your-domain.com`). 2. **Receive the domain association file.** Over Slack or email, you will receive the `apple-developer-merchantid-domain-association.txt` file generated for that domain. 3. **Host the file.** Place it at: ```text theme={null} https://your-domain.com/.well-known/apple-developer-merchantid-domain-association.txt ``` 4. **Push verifies the domain.** Once the file is reachable over HTTPS, your Push Cash counterpart verifies the domain in the Apple developer console. This must be done manually by Push — it is not self-serve. ## Go-live checklist * Production hostname (`api.pushcash.com`) is configured and all connections use HTTPS * API key is in the production format (`pcsk_production_...`) and `POST /keys/verify` returns the correct organization * Stable egress IPs supplied to Push Cash and the allowlist is configured * A write request from an allowlisted IP succeeds (and, optionally, one from an unrecognized IP is rejected with a `403`) * Production account ID(s) confirmed and verified via `GET /account/{id}` * **\[Apple Pay Web only]** Production cashier domain supplied to Push Cash * **\[Apple Pay Web only]** Domain association file hosted at `/.well-known/apple-developer-merchantid-domain-association.txt` and reachable over HTTPS * **\[Apple Pay Web only]** Push Cash has verified the domain in the Apple developer console ## Next steps If you have not already, set up webhooks to receive asynchronous updates about payment status. Refer to the [enabling webhooks](/enabling-webhooks) guide for details. Questions or issues going live? Reach out to your Push Cash representative or [hello@pushcash.com](mailto:hello@pushcash.com). # Ledger Source: https://docs.pushcash.com/ledger How Push records money movement as transactions, settles them into transfers on a fixed schedule, and how to reconcile it all against your own books. ## Overview Push maintains a ledger tracking all movement of money into and out of your product. Settlement begins when a payment is approved and posted to the payment network, and ends when the funds are deposited into your operational account. In between, Push records that movement as a **transaction** and settles it via a **transfer**. This guide explains the model your payments and finance teams will use to reconcile payments made through Push against your own records: what a transaction is, the lifecycle it moves through, how transactions are composed into transfers, the schedule on which they settle, and how to tie it all back to the payments you submitted. ## The model The ledger is built from three resources: A single record of money movement, one per approved intent, refund, or dispute. A net movement of funds between Push and your operational account, composed from a set of transactions. The managed accounts that transactions settle against. The relationship is straightforward: **each payment produces a transaction, and each transaction is settled by exactly one transfer.** A transfer's amount is the exact net of the transactions it contains. This one-to-one, sum-exact relationship is what makes the ledger reconcilable — you can always trace a transfer back to the precise set of payments that produced it. ## Transactions A transaction is a stateful record of money moving through the platform. Push creates a transaction when it commits to a movement of funds (i.e. when a payment intent is approved and submitted for processing). Every transaction has a **direction** and an **amount**: * `direction` is `cash_in` (a deposit or purchase moving funds from the user's account to the platform) or `cash_out` (a withdrawal, refund, or payout moving funds from your platform back to the user's bank account). * `amount` is an unsigned integer in the smallest currency unit (for USD, integer cents). The amount is always non-negative; the `direction` tells you which way the money moved. The amount is always positive. To compute a signed effect on your balance, treat `cash_in` as positive and `cash_out` as negative. ### The transaction fields | Field | Description | | :----------- | :----------------------------------------------------------------------------------------------------------------------------------- | | `id` | Unique identifier, prefixed `txn_`. | | `amount` | Unsigned amount in the smallest currency unit (USD: cents). | | `direction` | `cash_in` or `cash_out`. | | `currency` | Currency of the transaction (currently `USD`). | | `status` | The stage in the settlement lifecycle — see [below](#the-transaction-lifecycle). | | `type` | What created the transaction: `intent`, `refund`, or `dispute`. | | `source_id` | The ID of the intent, refund, or dispute that created it. Combine with `type` to find the source object. | | `account_id` | The account the transaction settles against. | | `created_at` | When the transaction was recorded (ISO 8601 timestamp). | | `date` | The settlement window (an Eastern-time day) in which the transaction was submitted. See [Settlement schedule](#settlement-schedule). | | `transfer` | The transfer that settles this transaction. Populated once the transaction is `in_transfer` or `settled`; `null` beforehand. | ### The transaction lifecycle A transaction advances through a series of statuses as its funds move toward settlement. The stages differ by direction, because `cash_in` funds must be confirmed by the financial network before they can be settled, whereas `cash_out` funds do not. ```mermaid theme={null} stateDiagram-v2 direction LR [*] --> pending pending --> available: cash_in — funds confirmed by network available --> settled: cash_in — settled in a transfer pending --> in_transfer: cash_out — batched into a transfer in_transfer --> settled: cash_out — network confirms funds received settled --> [*] ``` * **Cash-in:** `pending → available → settled` * **Cash-out:** `pending → in_transfer → settled` `available` applies only to `cash_in` (it marks funds confirmed by the network and ready to settle); `in_transfer` applies only to `cash_out` (it marks funds being collected via a transfer). | Status | Meaning | | :------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `pending` | The transaction has been recorded but the funds have not yet been confirmed by the network. For `cash_in`, the funds have not yet arrived; for `cash_out`, the payment has been recorded but not yet included in a transfer. | | `available` | **`cash_in` only.** The funds have been confirmed by the network and are eligible for settlement, but have not yet been placed into a transfer. | | `in_transfer` | **`cash_out` only.** The transaction has been batched into a transfer and the funds are being collected from your operational account. | | `settled` | Final status. The transfer containing the transaction has settled with the network — for `cash_in` the funds have been transmitted, and for `cash_out` the network has confirmed the counterparty received the funds. | A transaction advances only forward through these stages. A transaction that does not advance as expected (for example, one whose settlement window has passed without being included in a transfer) falls out of the normal flow and is resolved by Push operations — it will not silently settle for the wrong amount. ## Transfers A transfer is the actual movement of funds between the Push settlement account and your operational account. At the close of each [settlement window](#settlement-schedule), Push gathers the transactions that are eligible to settle, nets them, and creates a transfer. A transfer has a **direction** that reflects the net movement: * `disbursement` — a net payout **to** your operational account. This happens when `cash_in` (deposits) exceeds `cash_out` (withdrawals) for the window. * `collection` — a net withdrawal **from** your operational account. This happens when `cash_out` exceeds `cash_in`. The transfer `amount` is unsigned and equals the **exact net** of its constituent transactions: ``` transfer.amount = | Σ(cash_in transactions) − Σ(cash_out transactions) | ``` Every settled transaction belongs to **exactly one** transfer, and a transfer's `transactions` array is the complete, exact set that produced its amount. ### The transfer fields | Field | Description | | :------------- | :-------------------------------------------------------------------- | | `id` | Unique identifier, prefixed `transfer_`. | | `amount` | Unsigned net amount in the smallest currency unit (USD: cents). | | `currency` | Currency of the transfer (currently `USD`). | | `direction` | `disbursement` (payout to you) or `collection` (withdrawal from you). | | `date` | The settlement date of the transfer. | | `created_at` | When the transfer was created (ISO 8601 timestamp). | | `transactions` | The exact set of transactions settled by this transfer. | | `account` | The account the transfer settles against. | When a transfer is embedded in the `transfer` field of a transaction object, it appears as a compact summary (`id`, `amount`, `currency`, `direction`, `date`, `created_at`) without its own nested `transactions` array. Fetch the [transfer endpoints](./apireference/ledger/list-transfers) to retrieve the full transfer with its constituents. ## Accounts Transactions and transfers settle against a Push-managed **account** — this is where your settlement activity lands. List your accounts with [`list accounts`](./apireference/ledger/list-accounts). Accounts identify *where* activity settles; your settled position for any period is derived from the transactions and transfers that reference the account, as described in [Reconciliation](#reconciliation) below. ## Settlement schedule Which window a transaction settles in — and therefore when it reaches `settled` and appears in a transfer — is fixed by the settlement schedule. Push Cash defines a 24-hour settlement window for payment intents submitted between the hours of 4PM Eastern the previous day and 4PM Eastern on each day of processing. * For any `cash_in` payment intents, the funds will be settled 1 day later (`t+1`). * For any `cash_out` payment intents, the funds will be settled the same day (`t`). The network operator will settle transfers to your operational account same-day if a processing day falls outside of a weekend or bank holiday. Otherwise, the funds will be settled on the next banking day. Push Cash submits transfers to the payment network every day of the week, regardless of holidays or weekends. The following table and diagrams depict the settlement schedule for a payment intent submitted at different times of the day. | Direction            | Submission Time (Eastern) | Settlement Date | | -------------------- | -------------------------------- | ------------------ | | `cash_in` | `< 4PM` on 2024-10-01            | 2024-10-02 `(t+1)` | | `cash_in` | `> 4PM` on 2024-10-01 | 2024-10-03 `(t+1)` | | `cash_out` | `< 4PM` on 2024-10-01 | 2024-10-01 `(t)` | | `cash_out` | `> 4PM` on 2024-10-01 | 2024-10-02 `(t)` | ```mermaid theme={null} gantt title Settlement Timeline for cash_out Payment Intents axisFormat %b %d tickInterval 1day section Before 4PM Payment Submitted :done, submit-in-window, 2024-09-30T16:00:00, 1d Funds Settled :milestone, after submit-in-window, 0d section After 4PM Payment Submitted :done, submit-after-window, 2024-10-01T16:00:00, 1d Funds Settled :milestone, after submit-after-window, 0d ``` ```mermaid theme={null} gantt title Settlement Timeline for cash_in Payment Intents axisFormat %b %d tickInterval 1day section Before 4PM Payment Submitted :done, submit-in-window, 2024-09-30T16:00:00, 1d Funds Held :active, hold-in-window, after submit-in-window, 1d Funds Settled :milestone, after hold-in-window, 0d section After 4PM Payment Submitted :done, submit-after-window, 2024-10-01T16:00:00, 1d Funds Held :active, hold-after-window, after submit-after-window, 1d Funds Settled :milestone, after hold-after-window, 0d ``` ## Reconciliation Because a transfer is an exact composition of its transactions, you can reconcile Push against your own books deterministically. A typical monthly or per-window reconciliation looks like this: A `disbursement` is a deposit into your operational account; a `collection` is a debit from it. The `amount` and `date` on the transfer correspond to the movement you'll see at your bank. Fetch the transfer with [`get a transfer`](./apireference/ledger/retrieve-a-transfer) and read its `transactions` array. Confirm that the transactions net to the transfer amount: ``` Σ(cash_in amounts) − Σ(cash_out amounts) = ± transfer.amount ``` Each transaction carries a `type` and `source_id` pointing to the intent, refund, or dispute that created it. If you set a [`tag`](./api#idempotency) when you authorized the payment, you can retrieve the source object and match it to your internal identifier. You do not need to poll for the moment a transaction settles. Use [webhooks](./enabling-webhooks) for real-time payment outcomes, and use the ledger endpoints (`list transactions`, `list transfers`) to reconcile settled activity for a window or date range. ### Querying the ledger | To find… | Use | Notes | | :------------------------------------------------- | :------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------ | | All transactions in a date range | [`list transactions`](./apireference/ledger/list-transactions) | Filter by `date.after` / `date.before` (settlement window) or `created_at.after` / `created_at.before`. | | Transactions for one account | [`list transactions`](./apireference/ledger/list-transactions) | Filter by `account_id`. | | Transactions of one kind | [`list transactions`](./apireference/ledger/list-transactions) | Filter by `type` (`intent`, `refund`, `dispute`). | | A single transaction, including its transfer | [`get a transaction`](./apireference/ledger/get-a-transaction) | The `transfer` field is populated once the transaction is `in_transfer` or `settled`. | | Transfers in a date range | [`list transfers`](./apireference/ledger/list-transfers) | Filter by `date` or `created_at`. | | A single transfer and its constituent transactions | [`get a transfer`](./apireference/ledger/retrieve-a-transfer) | Returns the full `transactions` array. | List endpoints are cursor-paginated: pass the `next_cursor` from a response as the `cursor` parameter of the next request until it is `null`. # Quickstart Source: https://docs.pushcash.com/quickstart # Overview Time to complete: **\< 10 minutes** This guide walks you through your first sandbox payment end to end: create an API key, register a user, render the Push Widget, tokenize a test card, and authorize the payment. ## Create an API key In order to securely access the API, you must first exchange a temporary code for a durable API key. You can request your temporary code in the dedicated slack channel setup for your organization or by reaching out to [hello@pushcash.com](mailto:hello@pushcash.com). Once you have received your code, exchange it for an API key by making a request to the `keys/exchange` endpoint. The steps below use the sandbox host (`sandbox.pushcash.com`). For production, make the same requests to `api.pushcash.com`. ```bash theme={null} $ curl --request POST \ --url https://sandbox.pushcash.com/keys/exchange \ --data '{"code": "TEMPORARY_CODE"}' ``` The response should contain your API key which you can use to authenticate your requests to the Push API. ```text theme={null} Your API key is: pcsk_sandbox_SPBLtVaLWA.EeL0j4DBkp4hDvtuUDO1fACOdOTHzvOzRynMWeHtyuwQ ``` Save the key to an environment variable so the requests below can reuse it: ```bash theme={null} export APIKEY=pcsk_sandbox_... ``` ## 1. Create a user Register the user you'll transact on behalf of by calling [create-user](./apireference/user/create-user). Keep the returned `id` for use in the next step. ```bash theme={null} curl --request POST \ --url https://sandbox.pushcash.com/user \ --header 'Authorization: Bearer '$APIKEY \ --header 'Content-Type: application/json' \ --data ' { "name": { "first": "Alfred", "last": "Hitchcock" }, "email": "alfred@imdb.com", "address": { "address_line_1": "1609 10th Ave", "locality": "Bodega Bay", "administrative_area": "CA", "postal_code": "94923", "country": "US" }, "date_of_birth": "1899-08-13", "government_id": { "type": "passport", "last4": "7349" }, "phone_number": "(555) 681-3485", "tag": "quickstart-user-1", "identity_verified": true } ' ``` ```json theme={null} { "id": "user_lVpbPL0K1XIiHx0DxipRbD" } ``` ## 2. Render the widget and generate a token The Push Widget renders a secure card form and exchanges the card details for a short-lived `token`. Because the widget runs in the browser, you'll host it in a minimal webpage. First generate a widget URL for the user, then load that URL into a local page. Generate a widget URL by calling [create-user-url](./apireference/user/create-user-url) with the user's `id` (from step 1) and the payment `direction`: ```bash theme={null} curl --request POST \ --url https://sandbox.pushcash.com/user/user_lVpbPL0K1XIiHx0DxipRbD/url \ --header 'Authorization: Bearer '$APIKEY \ --header 'Content-Type: application/json' \ --data ' { "direction": "cash_in" } ' ``` ```json theme={null} { "url": "https://cdn.pushcash.com/widget/?param=1¶m=2¶m=3" } ``` Next, create a folder with the two files below. `index.html` loads the [Push JS SDK](./sdk) and provides a field to paste the widget URL into; `app.js` renders the widget and tokenizes the card. ```html index.html theme={null} Push Widget Quickstart

Push Widget Quickstart



    
    
    
  

```

```js app.js theme={null}
const widgetUrl = document.getElementById('widget-url');
const tokenizeButton = document.getElementById('tokenize-button');
const result = document.getElementById('result');

let widget;

// Render (or re-render) the widget whenever a URL is entered.
widgetUrl.addEventListener('input', () => {
  if (widget) {
    widget.destroy();
    widget = undefined;
  }
  tokenizeButton.disabled = true;

  if (!widgetUrl.value.trim()) return;

  widget = new window.PushCash.Widget({
    selector: '#widget-container',
    url: widgetUrl.value.trim(),
    // Enable the button once the card details are valid.
    onValid: () => {
      tokenizeButton.disabled = false;
    },
  });
});

// Exchange the card details for a token.
tokenizeButton.addEventListener('click', async () => {
  try {
    const { token } = await widget.tokenize();
    result.textContent = 'Token: ' + token;
  } catch (err) {
    result.textContent = 'Tokenization failed: ' + err.message;
  }
});
```

Then:

1. Open `index.html` directly in your browser. You'll need an internet connection so the SDK can load from the CDN.
2. Paste the `url` from the previous step into the **Widget URL** field and verify the card form appears.
3. Enter the approved sandbox test card `5555 5555 5555 4444`. Use any future date for expiration and any 3-digit value for CVV.
4. Click **Generate token**. The page displays a `token` — copy it for the next step.

## 3. Authorize the payment

Submit the `token` to [authorize-payment](./apireference/authorization/authorize-payment) with the payment details to process the transaction:

```bash theme={null}
curl --request POST \
	--url https://sandbox.pushcash.com/authorize \
	--header 'Authorization: Bearer '$APIKEY \
	--header 'Content-Type: application/json' \
	--data '
{
  "amount": 2500,
  "currency": "USD",
  "direction": "cash_in",
  "token": "token_mbDRHFi3dxIZEtykHsgUGC"
}
'
```

A `200 OK` response confirms the payment was approved:

```json theme={null}
{
  "id": "intent_sandbox_mbDRHFi3dxIZEtykHsgUGC",
  "amount": 2500,
  "direction": "cash_in",
  "currency": "USD",
  "credential": {
    "id": "cred_7YlA9IiSl8UZvNwNSbFajV",
    "display_name": "Visa 4444",
    "last4": "4444"
  }
}
```

Congratulations, you've processed your first payment 🎉

## Next steps

* Build the production flow (including processing stored credentials, handling step-up authentication, support for withdrawals and refunds) with the [Card Payments](./card-payments) guide.


# React Native SDK
Source: https://docs.pushcash.com/react-native-sdk



React Native bindings for the Push platform: a `` component that renders the card-entry form and tokenizes the card. When [authorize-payment](./apireference/authorization/authorize-payment) returns `202 Accepted`, [complete authentication](#completing-authentication) by opening the returned URL in an in-app browser tab.

For the payment flow itself — registering users, authorizing, handling declines, withdrawals, refunds — follow the [Card Payments](./card-payments) guide. This page documents only the client-side surface.

## Open source

The SDK is published from a public repository at [github.com/pushcashco/react-native](https://github.com/pushcashco/react-native) under the MIT license, with no closed-source binaries.

## Install

```sh theme={null}
npm install @pushcashco/react-native react-native-webview
```

`react` (`>=18.2`), `react-native` (`>=0.74 <1.0.0`), and `react-native-webview` (`>=13.8 <17`) are peer dependencies. The package contains no native code, so it sets no minimum iOS or Android version of its own — yours are whatever your React Native version requires.

The package is pure JavaScript, so it runs in Expo Go with no development build. In a bare React Native app, run `npx pod-install ios` for `react-native-webview`.

## ``

Renders the Push card-entry form. Collect a card, then call `tokenize()` through a ref from your submit handler to get a one-time token for [authorize-payment](./apireference/authorization/authorize-payment). Enable your submit button from `onValid`.

The form is Push-hosted content rendered in a WebView, so card details are entered into Push's page and never pass through your application code.

### Props (`PushWidgetProps`)

| Prop           | Type                     | Required | Description                                                                                                                                                                                     |
| -------------- | ------------------------ | :------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `url`          | `string`                 |     ✅    | URL returned from [create-user-url](./apireference/user/create-user-url). Pass `type: "secure_debit"` when collecting a card for a withdrawal — see [Withdrawals](./card-payments#withdrawals). |
| `onValid`      | `() => void`             |     ❌    | Called once the entered card is valid. Use it to enable your submit button.                                                                                                                     |
| `onError`      | `(error: Error) => void` |     ❌    | Called if the form fails to load, with a message describing what went wrong.                                                                                                                    |
| `style`        | `StyleProp`   |     ❌    | Layout style for the container — width, margins, positioning. The component sets its own height to fit the form.                                                                                |
| `background`   | `string`                 |     ❌    | Form background (color string, e.g. `#ffffff`).                                                                                                                                                 |
| `fontSize`     | `number \| string`       |     ❌    | Base font size. A `number` is treated as density-independent pixels; a `string` is passed through as a CSS value.                                                                               |
| `borderRadius` | `number \| string`       |     ❌    | Corner radius.                                                                                                                                                                                  |
| `padding`      | `number \| string`       |     ❌    | Inner padding.                                                                                                                                                                                  |
| `color`        | `string`                 |     ❌    | Text color (color string).                                                                                                                                                                      |

### Ref (`PushWidgetRef`)

Attach a ref to call:

| Method     | Type                               | Description                                                                                                                                                                                                                  |
| ---------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tokenize` | `() => Promise<{ token: string }>` | Generates a one-time token from the entered card for [authorize-payment](./apireference/authorization/authorize-payment). Rejects with an `Error` describing what went wrong — an invalid card, or a failure to tokenize it. |

### Example

```tsx theme={null}
import { PushWidget } from '@pushcashco/react-native';
import type { PushWidgetRef } from '@pushcashco/react-native';
import { useRef, useState } from 'react';
import { Button, View } from 'react-native';

function CardForm({ url }: { url: string }) {
  const widget = useRef(null);
  const [ready, setReady] = useState(false);

  return (
    
       setReady(true)}
        style={{ marginHorizontal: 16 }}
        borderRadius={12}
        padding={16}
      />
      
    
  );
}
```

## ``

Presents the payment UX for a single intent in a modal — a centered dialog on desktop, a full-page sheet on mobile. Render it once you hold an intent `id` (typically after your submit handler's `POST /authorize` returns `202 Accepted`); it opens on mount and renders no markup of its own. Unmount it to end the flow.

### Props (`PushUXProps`)

| Prop       | Type         | Required | Description                                                                                                                                                                          |
| ---------- | ------------ | :------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `intentId` | `string`     |     ✅    | The intent `id` returned by [authorize-payment](./apireference/authorization/authorize-payment) with a `202 Accepted` response.                                                      |
| `onExit`   | `() => void` |     ✅    | Called exactly once when the flow ends — whether the user completes it or dismisses the modal. It carries no result; retrieve the authoritative outcome from the server (see below). |

`onExit` does not indicate whether the payment succeeded — call [get-an-intent](./apireference/intent/get-an-intent) (or wait for the intent webhook) and inspect `status`.

### Example

```tsx theme={null}
import { PushUX } from '@pushcash/react';

function Checkout({ intentId }: { intentId: string }) {
  return (
     {
        // the flow has ended — fetch the result from your backend
        const intent = await yourBackend.getIntent(intentId);
        console.log(intent.status); // 'approved' | 'declined' | ...
      }}
    />
  );
}
```

Render `` only when you have an intent `id`. A typical checkout collects the card with `` above, calls `POST /authorize` on submit, and on a `202 Accepted` renders `` with the returned `id`.


# JS SDK
Source: https://docs.pushcash.com/sdk



## Push JS SDK Reference The SDK must be loaded as a script tag on your website — it cannot be statically bundled via package management. ```html theme={null} ```
The SDK is loaded from `cdn.pushcash.com` and creates iframes that load content from the same origin. If your site uses a [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP), ensure the following directives include `https://cdn.pushcash.com`: * `script-src` — to load the SDK script * `frame-src` — to allow the widget and UX iframes ## Widget ### `new window.PushCash.Widget(options)` Creates the Push User Widget. The widget renders a card input form into the container specified by `selector`. #### Options (`WidgetOptions`) | Field | Type | Required | Description | | -------------- | ------------ | :------: | ---------------------------------------------------------------------------------------- | | `url` | `string` | ✅ | URL returned from [create-user-url](./apireference/user/create-user-url). | | `selector` | `string` | ✅ | CSS selector for the container element to render into. | | `onValid` | `() => void` | ❌ | Called once the widget has received valid card details. Use to enable the submit button. | | `background` | `string` | ❌ | Widget background (CSS color value). | | `fontSize` | `string` | ❌ | Base font size (CSS value, e.g. `14px`). | | `borderRadius` | `string` | ❌ | Corner radius (CSS value, e.g. `12px`). | | `padding` | `string` | ❌ | Inner padding (CSS value, e.g. `16px`). | | `color` | `string` | ❌ | Text color (CSS color value). | #### Returns A `Widget` instance. #### Example ```js theme={null} const widget = new window.PushCash.Widget({ url: 'https://cdn.pushcash.com/widget/?param=1¶m=2', selector: '#payment-container', onValid: () => { console.log('user has provided valid card details') }, background: '#ffffff', fontSize: '16px', borderRadius: '8px', padding: '12px', color: '#000000', }); ``` ### `widget.tokenize()` Generates a token from the user’s card details which can be used to process a transaction by calling [authorize-payment](./apireference/authorization/authorize-payment). Throws an error if the user has not provided valid card details. #### Signature ```ts theme={null} tokenize: () => Promise<{ token: string }> ``` #### Example ```js theme={null} try { const { token } = await widget.tokenize(); } catch (error) { console.error(‘User has not provided valid card details:’, error); } ``` ### `widget.destroy()` Unmounts the widget and cleans up resources. #### Signature ```ts theme={null} destroy: () => void ``` *** ## UX Presents the Push-hosted authentication UI for an intent in a modal. Use it on the web to complete the step-up authentication requested when [authorize-payment](./apireference/authorization/authorize-payment) returns `202 Accepted` — see [Presenting the authentication UI](./card-payments#presenting-the-authentication-ui). ### `window.PushCash.UX(config)` Creates a UX handler for a single intent. It renders the hosted UX in a modal — a centered dialog on desktop, a full-page sheet on mobile — and manages its lifecycle. #### Config (`PushUXConfig`) | Field | Type | Required | Description | | ---------- | ------------ | :------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `intentId` | `string` | ✅ | The intent `id` returned by [authorize-payment](./apireference/authorization/authorize-payment) with a `202 Accepted` response. | | `onExit` | `() => void` | ✅ | Called exactly once when the flow ends — whether the user completes it or dismisses the modal. It carries no result; retrieve the authoritative outcome from the server (see below). | Because you omit `redirect_url` when presenting on the web, the flow's end is signaled through `onExit` rather than a redirect. `onExit` does not indicate whether the payment succeeded — call [get-an-intent](./apireference/intent/get-an-intent) (or wait for the intent webhook) and inspect `status`. #### Returns A `PushUXHandler`. #### Example ```js theme={null} const ux = window.PushCash.UX({ // the intent id returned from the authorize-payment 202 response intentId: 'intent_sandbox_dMggQ93ZYH6DH9LBhVeijE', onExit: async () => { // the flow has ended — fetch the result from your backend const intent = await yourBackend.getIntent('intent_sandbox_dMggQ93ZYH6DH9LBhVeijE'); console.log(intent.status); // 'approved' | 'declined' | ... }, }); ux.open(); ``` ### `handler.open()` Presents the modal and begins the flow. Calling `open()` again while the modal is already open, or after the flow has ended, is a no-op. #### Signature ```ts theme={null} open: () => void ``` ### `handler.exit()` Ends the flow programmatically: tears down the modal and fires `onExit` (once). Use it to close the UX from your own UI. #### Signature ```ts theme={null} exit: () => void ``` ### `handler.destroy()` Removes the modal and all listeners **without** firing `onExit`. Terminal and idempotent — after `destroy()` the handler is spent and `open()` does nothing. Use it for cleanup (for example, when unmounting your page) where teardown should not be treated as an exit. #### Signature ```ts theme={null} destroy: () => void ``` *** ## Apple Pay Launcher ### `new window.PushCash.ApplePay(config)` Creates an Apple Pay launcher instance. #### Parameters | Field | Type | Required | Description | | ----- | -------- | :------: | --------------------------------------------------------------------------------------------------------------------------------------- | | `url` | `string` | ✅ | URL returned from [create-user-url](./apireference/user/create-user-url) with `type: "apple_pay"`. Must be newly created per page load. | #### Example ```js theme={null} const launcher = new window.PushCash.ApplePay({ url: '', }); ``` ### `launcher.display(args)` Launches the Apple Pay payment sheet and runs the Apple Pay flow. #### Parameters | Field | Type | Required | Description | | ------------- | ------------------------------------------------------ | :------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `amount` | `number` | ✅ | Payment amount in **cents** (must be > 0). | | `direction` | `'cash_in'` | ✅ | Only `cash_in` is supported. | | `currency` | `'USD'` | ✅ | Only `"USD"` is supported. | | `onAuthorize` | `(token: string) => Promise<'approved' \| 'declined'>` | ✅ | Called after the user authorizes payment in the Apple Pay sheet. Receives a `token` which must be passed to [authorize payment](./apireference/authorization/authorize-payment). Must return `'approved'` or `'declined'` based on the authorization result. The amount and currency must match the values provided to `display()`. | | `onComplete` | `() => void \| Promise` | ❌ | Called when the Apple Pay session completes. | #### Example ```js theme={null} launcher.display({ amount: 1000, direction: 'cash_in', currency: 'USD', onAuthorize: async (token) => { // call backend to authorize payment via Push API // pass token to POST /authorize const result = await yourBackend.authorize(token); return result.status; // 'approved' or 'declined' }, onComplete: () => { // handle session completion }, }); ``` # Welcome Source: https://docs.pushcash.com/welcome-guide Build with Push Cash — from your first sandbox transaction to a production-ready payments integration. Push Cash is a payments platform for moving money into and out of your product across cards, Apple Pay, and ACH. These docs take you from your first sandbox call to a production-ready integration. ## Start here New to Push Cash? Begin with the **[Quickstart](./quickstart)** in order to create an API key and start processing simulated payments in the sandbox environment. From there, **[Card Payments](./card-payments)** is the foundation of most integrations. ## Build your integration The core integration guides cover everything needed to accept and pay out money. Start with Card Payments; the others layer on additional payment methods and the asynchronous updates that keep your system in sync.

Card Processing

Guaranteed debit transactions, no chargeback liability, authorization rates competitors can't match.

Saving Declined Transactions

No processor switch or changes to your payments stack. Bolt-on logic for recovering declined card transactions.

## Go live When your sandbox integration is complete, work through the **[Go-Live Checklist](./go-live-checklist)** on a call with your Push Cash representative to set up production API access, account identifiers, and domain verification for Apple Pay. ## Reference Detailed technical references to use alongside the guides: Every endpoint, request, and response for the Push API. The Push JS SDK that powers the Widget and the Apple Pay launcher.