Build a Payment · Step 4 of 5

Take payment

Get the hosted-checkout URL with the user_token, hand it to the buyer, then wait for the PAID webhook.

  1. 1Auth
  2. 2Users
  3. 3Contract
  4. 4Payment
  5. 5Settle

Start payment

Use USER_TOKEN , not CUSTOMER_TOKEN. This returns a hosted checkout URL.

bash
curl -s "$WAFFY_BASE_URL/api/external/contracts/startPayment/$MILESTONE_ID/$WAFFY_CLIENT_ID?redirectUrl=https://yourapp.com/done&paymentType=PURCHASE" \
  -H "Authorization: Bearer $CUSTOMER_TOKEN"

Response:

json
{
  "data": "https://app.waffyapp.com/external/UXFmUCaG?client_id=...&payment_methods=MADA,VISA,APPLE_PAY"
}

payment_methods is auto-populated from your org's contracted methods — you do not set it.

Two ways to present the checkout URL — pick one based on your front-end stack.

Option A — Raw URL redirect (Flutter and frameworks the SDK doesn't cover)

Append userTokenUrl as a query parameter to the returned URL and redirect the buyer there.

javascript
const url = paymentUrl + "&userTokenUrl=" + CUSTOMER_TOKEN;
window.location.href = url;  // or Flutter: launchUrl(Uri.parse(url))

Option B — Waffy SDK (recommended for Web, Android, iOS, React Native)

The SDK handles the checkout in a modal / popup / iframe and manages auth for you. Include the SDK script from https://sdk.waffyapp.com/v2/waffy-payment-display.min.js in your page, then call:

javascript
WaffyPaymentDisplay.show({
  paymentUrl: "https://app.waffyapp.com/external/UXFmUCaG?client_id=...",
  userToken: CUSTOMER_TOKEN,
  mode: "modal"  // or "popup" or "iframe"
});

Android / iOS / React Native use the platform equivalents of the same SDK — your integration team receives the package references during onboarding.

Handle the PAID event

When the buyer completes payment, you will receive:

json
{
  "contractId": "607f1f77bcf86cd799439022",
  "status": "PAID",
  "referenceId": "txn_abc123"
}

contractId is the milestone ID. Always verify the Waffy-Signature header before processing.

Webhook URL is configured by Waffy at onboarding

You don't PATCH it yourself. Give your endpoint URL to your account manager during sandbox set-up and again before go-live. If it ever needs to change, email developers@waffyapp.com.

Verify the webhook signature

Every webhook request carries a Waffy-Signature header. Compute HMAC-SHA256 of the raw request body using your webhook secret and compare. Reject any request that does not match.

bash
# Header format
Waffy-Signature: sha256=abc123def456...

# Verification (Node.js example)
const expected = "sha256=" + crypto
  .createHmac("sha256", WEBHOOK_SECRET)
  .update(rawBody)
  .digest("hex");

if (expected !== req.headers["waffy-signature"]) {
  return res.status(401).send("Signature mismatch");
}

Retry policy

Waffy retries failed webhook deliveries 3 times: after 1 s, 10 s, and 100 s. Your endpoint must respond HTTP 200 immediately — process the event asynchronously.

Respond 200 first, process second

If your endpoint takes more than a few seconds to respond, Waffy will mark the delivery as failed and retry. Acknowledge immediately with HTTP 200, then enqueue the event for async processing.

Webhook status values

StatusMeaning
PAIDBuyer paid — funds are in escrow. Move to delivery.
CASHOUT_IN_PROGRESSFunds are being released to parties. No action needed.
COMPLETEDContract is fully settled. All parties have been paid out.