1. Transactions
Synswi
  • Guides
    • Getting Started With the API
  • API Reference
    • Payment instruments
      • Create a payment instrument
      • Get payment instruments
      • Get a payment instrument by id
      • Create a Payment Instrument
    • Transactions
      • Cash App Pay Integration Guide
      • Get a transaction by id
        GET
      • Get transactions
        GET
      • Create a transaction
        POST
      • Update a transaction
        POST
      • Refund a transaction
        POST
      • Create a cash-app transaction
        POST
    • Fees
      • Get current fee profile
    • Programs
      • Accounts
    • Events
      • Get Events
  1. Transactions

Cash App Pay Integration Guide

This guide explains how to integrate Cash App Pay using the Synswi Transaction API and Stripe Payment Element.

1. Payment Flow#

The customer opens the merchant's payment page
        |
        v
The merchant backend calls POST /transactions
        |
        v
Synswi creates a Stripe PaymentIntent
        |
        v
Synswi returns clientSecret and stripeConnectedAccountId
        |
        v
The merchant frontend initializes Stripe.js and displays Payment Element
        |
        v
The customer confirms the Cash App Pay payment
        |
        v
Stripe sends an asynchronous webhook to Synswi
        |
        v
Synswi creates a succeeded transaction with the merchant's tags
Important behavior:
A successful POST /transactions response means that the PaymentIntent was created. It does not mean that the customer completed the payment.
Synswi creates the final transaction after receiving a successful Stripe webhook.
Use tags to associate the payment with an order in your system.
Cash App Pay does not require a paymentInstrumentId.

2. Environment#

Development API base URL:
https://api-dev.synswi.com
Create payment endpoint:
Required headers:
Synswi will provide the Stripe platform Publishable Key separately. The merchant must never receive or use a Stripe Secret Key, Connected Account Secret Key, or Webhook Secret.

3. Authentication#

Obtain an Access Token from the Synswi login endpoint and include it in every protected API request:
For Apidog, save the token as an environment variable:
AUTH_TOKEN = eyJ...
Then configure the project-level or folder-level Bearer Token as:
{{AUTH_TOKEN}}

4. Create a Cash App Pay Payment#

4.1 Request Fields#

FieldTypeRequiredExampleDescription
accountIdstringYesacc-123Synswi account ID
amountintegerYes1280Amount in the smallest currency unit; 1280 means USD 12.80
typestringYespullMust be pull for Cash App Pay
channelstringYesonlineMust be online for Cash App Pay
methodstringYescashappMust be cashapp
tagsobjectNo, but recommended{ "orderId": "ORDER-10001" }Merchant-defined fields used to associate the transaction with an order
Values inside tags are stored as strings. We recommend including at least one unique merchant order ID:
{
  "tags": {
    "orderId": "ORDER-10001",
    "customerId": "CUSTOMER-20001"
  }
}

4.2 cURL Example#

4.3 Merchant Backend Example#

The merchant backend should call the Synswi API. Do not expose a long-lived Synswi Access Token in browser code.
type CreateCashAppPaymentRequest = {
  accountId: string;
  amount: number;
  orderId: string;
  customerId?: string;
};

type CreateCashAppPaymentResponse = {
  clientSecret: string;
  stripeConnectedAccountId: string;
};

export async function createCashAppPayment(
  request: CreateCashAppPaymentRequest,
): Promise<CreateCashAppPaymentResponse> {
  const response = await fetch("https://api-dev.synswi.com/transactions", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SYNSWI_ACCESS_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      accountId: request.accountId,
      amount: request.amount,
      type: "pull",
      channel: "online",
      method: "cashapp",
      tags: {
        orderId: request.orderId,
        ...(request.customerId
          ? { customerId: request.customerId }
          : {}),
      },
    }),
  });

  const data = await response.json();

  if (!response.ok) {
    throw new Error(data.message || "Unable to create Cash App payment");
  }

  return data;
}

4.4 Successful Response#

HTTP status: 200 OK
{
  "clientSecret": "pi_xxx_secret_xxx",
  "stripeConnectedAccountId": "acct_xxx"
}
FieldDescription
clientSecretUsed by the frontend to initialize Stripe Elements. Only provide it to the customer completing this payment.
stripeConnectedAccountIdThe Stripe Connected Account on which the PaymentIntent was created.
Do not log the complete clientSecret, put it in a URL, or expose it to anyone other than the customer completing the payment.

5. Confirm the Payment on the Frontend#

5.1 Install Stripe.js#

5.2 Initialize Stripe#

The frontend must use both:
The Synswi Stripe platform Publishable Key.
The stripeConnectedAccountId returned by POST /transactions.
import { loadStripe } from "@stripe/stripe-js";

const stripePromise = loadStripe(
  "<SYNSWI_PLATFORM_PUBLISHABLE_KEY>",
  {
    stripeAccount: stripeConnectedAccountId,
  },
);
If stripeAccount is missing, Stripe might return:
The client_secret provided does not match any associated PaymentIntent on this account.
This happens because the PaymentIntent belongs to the Connected Account while Stripe.js is trying to find it on the platform account.

5.3 React Payment Element Example#

import {
  Elements,
  PaymentElement,
  useElements,
  useStripe,
} from "@stripe/react-stripe-js";
import { loadStripe } from "@stripe/stripe-js";
import { FormEvent, useMemo } from "react";

function PaymentForm() {
  const stripe = useStripe();
  const elements = useElements();

  async function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();

    if (!stripe || !elements) return;

    const { error } = await stripe.confirmPayment({
      elements,
      confirmParams: {
        return_url: "https://merchant.example.com/payment/result",
      },
    });

    if (error) {
      console.error(error.code, error.message);
      // Display error.message near the payment form.
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <PaymentElement />
      <button type="submit" disabled={!stripe}>
        Pay with Cash App
      </button>
    </form>
  );
}

type PaymentPageProps = {
  clientSecret: string;
  stripeConnectedAccountId: string;
};

export function PaymentPage(props: PaymentPageProps) {
  const stripePromise = useMemo(
    () =>
      loadStripe("<SYNSWI_PLATFORM_PUBLISHABLE_KEY>", {
        stripeAccount: props.stripeConnectedAccountId,
      }),
    [props.stripeConnectedAccountId],
  );

  return (
    <Elements
      stripe={stripePromise}
      options={{ clientSecret: props.clientSecret }}
    >
      <PaymentForm />
    </Elements>
  );
}
The frontend cannot add or modify PaymentIntent metadata through stripe.confirmPayment(). Send order-related information to Synswi through tags when creating the payment.

6. Payment Result Processing#

Cash App Pay can redirect the customer to Cash App or to a Stripe test confirmation page. After the payment attempt, Stripe redirects the customer to the configured return_url and may add these query parameters:
payment_intent
payment_intent_client_secret
redirect_status
The merchant may use redirect_status=succeeded to display a result to the customer, but must not use it as the only source of truth for fulfillment or accounting.
The final payment result is delivered to Synswi through a Stripe webhook. When Synswi receives payment_intent.succeeded, it:
1.
Verifies the Stripe webhook signature.
2.
Reads accountId and the transaction tags from PaymentIntent metadata.
3.
Creates a transaction with status: "succeeded".
4.
Stores the Stripe PaymentIntent ID as the payment reference.
5.
Creates the applicable transaction fee record.

7. Look Up the Final Transaction#

Use the unique orderId sent in tags to find the final transaction.
Request example:
{
  "accountId": "acc-123",
  "tags": {
    "orderId": "ORDER-10001"
  },
  "limit": 10
}
Response example:
{
  "transactions": [
    {
      "id": "txn-xxx",
      "accountId": "acc-123",
      "amount": 1280,
      "currency": "USD",
      "type": "pull",
      "subtype": "payment",
      "channel": "online",
      "method": "cashapp",
      "status": "succeeded",
      "referenceId": "stripe-pi_xxx",
      "tags": {
        "orderId": "ORDER-10001",
        "customerId": "CUSTOMER-20001"
      }
    }
  ],
  "totalCount": 1
}
Webhook processing is asynchronous, so the transaction might not exist immediately after the customer returns to the merchant page. A simple approach is to poll every 2 seconds for up to 30 seconds. If the result is still unavailable, display "Payment confirmation pending" and do not automatically create another payment.

8. HTTP Errors#

API errors normally use this response format:
{
  "message": "Error description"
}
HTTP statusCommon messageRecommended action
400accountId is requiredProvide the Synswi account ID.
400amount is requiredProvide the payment amount.
400amount must be greater than 0Use an amount greater than zero.
400amount must be an integerSend the amount in the smallest currency unit as an integer.
400type must be pull or pushUse pull for Cash App Pay.
400Channel is invalidUse online for Cash App Pay.
400cashapp is only supported for online pull transactionsCash App Pay cannot be used for an in-person transaction.
400Method is invalidCheck the requested payment method.
400Invalid accountVerify the accountId.
400cashapp requires a Stripe card processorThe account is not configured to process Cash App Pay through Stripe. Contact Synswi.
400Stripe connected account is required for cashapp transactionsThe account is missing its Stripe Connected Account configuration. Contact Synswi.
400Provider apiKey is requiredThe Synswi Stripe provider configuration is incomplete. Contact Synswi.
401Invalid tokenThe token is missing, invalid, or expired. Obtain a new token.
403User does not have the required permissions to perform this actionThe authenticated user does not have the required account permissions.
403Account is not active, account status: ...The account must be active before accepting payments.
404Account not foundVerify the accountId.
422Payment failure informationPrimarily used for synchronous payment failures. It is not normally returned while creating a Cash App PaymentIntent.
500Unknown internal server errorRecord the request time, merchant order ID, and response, then contact Synswi.

Stripe.js Frontend Errors#

stripe.confirmPayment() returns a Stripe error object. It does not necessarily correspond to an HTTP response from the Synswi API.
const { error } = await stripe.confirmPayment(...);

console.log(error?.type);
console.log(error?.code);
console.log(error?.decline_code);
console.log(error?.message);
SituationRecommended action
Payment Element validation errorDisplay error.message near the payment form.
Customer cancels Cash App authorizationAllow the customer to try again without creating a new merchant order.
PaymentIntent account mismatchConfirm that Stripe.js was initialized with the returned stripeConnectedAccountId.
Network failure or unknown resultLook up the transaction before creating another PaymentIntent.

9. Integration Checklist#

A valid Synswi Access Token is available.
The accountId has a Stripe Card Processor and Connected Account configured.
The amount is an integer in cents; for example, USD 12.80 is 1280.
The request uses type=pull, channel=online, and method=cashapp.
tags.orderId is unique in the merchant system.
Stripe.js uses the Synswi platform Publishable Key.
Stripe.js is initialized with the returned stripeConnectedAccountId.
Payment Element uses the returned clientSecret.
The production payment page uses HTTPS.
The payment button is disabled while a request is being submitted.
Business fulfillment uses the final transaction as its source of truth.
The complete clientSecret is not written to logs, URLs, or monitoring tools.

10. References#

Stripe Cash App Pay with Payment Element
Stripe Payment Element
Previous
Create a Payment Instrument
Next
Get a transaction by id
Built with