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 tagsPOST /transactions response means that the PaymentIntent was created. It does not mean that the customer completed the payment.tags to associate the payment with an order in your system.paymentInstrumentId.https://api-dev.synswi.comAUTH_TOKEN = eyJ...{{AUTH_TOKEN}}| Field | Type | Required | Example | Description |
|---|---|---|---|---|
accountId | string | Yes | acc-123 | Synswi account ID |
amount | integer | Yes | 1280 | Amount in the smallest currency unit; 1280 means USD 12.80 |
type | string | Yes | pull | Must be pull for Cash App Pay |
channel | string | Yes | online | Must be online for Cash App Pay |
method | string | Yes | cashapp | Must be cashapp |
tags | object | No, but recommended | { "orderId": "ORDER-10001" } | Merchant-defined fields used to associate the transaction with an order |
tags are stored as strings. We recommend including at least one unique merchant order ID:{
"tags": {
"orderId": "ORDER-10001",
"customerId": "CUSTOMER-20001"
}
}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;
}200 OK{
"clientSecret": "pi_xxx_secret_xxx",
"stripeConnectedAccountId": "acct_xxx"
}| Field | Description |
|---|---|
clientSecret | Used by the frontend to initialize Stripe Elements. Only provide it to the customer completing this payment. |
stripeConnectedAccountId | The Stripe Connected Account on which the PaymentIntent was created. |
clientSecret, put it in a URL, or expose it to anyone other than the customer completing the payment.stripeConnectedAccountId returned by POST /transactions.import { loadStripe } from "@stripe/stripe-js";
const stripePromise = loadStripe(
"<SYNSWI_PLATFORM_PUBLISHABLE_KEY>",
{
stripeAccount: stripeConnectedAccountId,
},
);stripeAccount is missing, Stripe might return:The client_secret provided does not match any associated PaymentIntent on this account.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>
);
}stripe.confirmPayment(). Send order-related information to Synswi through tags when creating the payment.return_url and may add these query parameters:payment_intent
payment_intent_client_secret
redirect_statusredirect_status=succeeded to display a result to the customer, but must not use it as the only source of truth for fulfillment or accounting.payment_intent.succeeded, it:accountId and the transaction tags from PaymentIntent metadata.status: "succeeded".orderId sent in tags to find the final transaction.{
"accountId": "acc-123",
"tags": {
"orderId": "ORDER-10001"
},
"limit": 10
}{
"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
}{
"message": "Error description"
}| HTTP status | Common message | Recommended action |
|---|---|---|
400 | accountId is required | Provide the Synswi account ID. |
400 | amount is required | Provide the payment amount. |
400 | amount must be greater than 0 | Use an amount greater than zero. |
400 | amount must be an integer | Send the amount in the smallest currency unit as an integer. |
400 | type must be pull or push | Use pull for Cash App Pay. |
400 | Channel is invalid | Use online for Cash App Pay. |
400 | cashapp is only supported for online pull transactions | Cash App Pay cannot be used for an in-person transaction. |
400 | Method is invalid | Check the requested payment method. |
400 | Invalid account | Verify the accountId. |
400 | cashapp requires a Stripe card processor | The account is not configured to process Cash App Pay through Stripe. Contact Synswi. |
400 | Stripe connected account is required for cashapp transactions | The account is missing its Stripe Connected Account configuration. Contact Synswi. |
400 | Provider apiKey is required | The Synswi Stripe provider configuration is incomplete. Contact Synswi. |
401 | Invalid token | The token is missing, invalid, or expired. Obtain a new token. |
403 | User does not have the required permissions to perform this action | The authenticated user does not have the required account permissions. |
403 | Account is not active, account status: ... | The account must be active before accepting payments. |
404 | Account not found | Verify the accountId. |
422 | Payment failure information | Primarily used for synchronous payment failures. It is not normally returned while creating a Cash App PaymentIntent. |
500 | Unknown internal server error | Record the request time, merchant order ID, and response, then contact Synswi. |
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);| Situation | Recommended action |
|---|---|
| Payment Element validation error | Display error.message near the payment form. |
| Customer cancels Cash App authorization | Allow the customer to try again without creating a new merchant order. |
| PaymentIntent account mismatch | Confirm that Stripe.js was initialized with the returned stripeConnectedAccountId. |
| Network failure or unknown result | Look up the transaction before creating another PaymentIntent. |
accountId has a Stripe Card Processor and Connected Account configured.1280.type=pull, channel=online, and method=cashapp.tags.orderId is unique in the merchant system.stripeConnectedAccountId.clientSecret.clientSecret is not written to logs, URLs, or monitoring tools.