Take payment
Get the hosted-checkout URL with the client admin token, get a payment ticket for the buyer, redirect them with the ticket, then wait for the PAID webhook.
- 1Auth
- 2Users
- 3Contract
- 4Payment
- 5Settle
1. Start payment
Use the client admin token. The org token is not accepted here. This returns the hosted checkout URL for the milestone. Use the same client you signed in with when you created the contract.
curl -s -G "$API_BASE_URL/api/external/contracts/startPayment/$MILESTONE_ID/$CLIENT_ID" \ -H "Authorization: Bearer $CLIENT_ADMIN_TOKEN" \ --data-urlencode "redirectUrl=https://yourapp.com/done" \ --data-urlencode "paymentType=PURCHASE"
Response:
{
"data": "https://external.waffyapp.com/external/UXFmUCaG?client_id=...&payment_methods=MANUAL_BANK_TRANSFER,MOYASAR,TABBY,APPLE_PAY"
}- The host is your environment's checkout host (for example
external-dev.waffyapp.comin the sandbox). payment_methodsis filled in from your organization's contracted methods. You do not set it.- The path segment after
/external/(hereUXFmUCaG) is the contract'sshortId. You need it for the ticket. - A milestone id that does not exist currently returns a bare
500, not404.
2. Get a payment ticket
Right before the redirect, ask for a ticket for this buyer and this payment. The buyer must have given consent (see Customers & consent).
curl -s -X POST "$ID_BASE_URL/realms/waffy/protocol/openid-connect/token" \ -H "Accept: application/json" \ --data-urlencode "grant_type=urn:waffy:params:oauth:grant-type:customer-token-exchange" \ --data-urlencode "client_id=$CLIENT_ID" \ --data-urlencode "client_secret=$CLIENT_SECRET" \ --data-urlencode "login_hint=$BUYER_PHONE" \ --data-urlencode "scope=payment:$PAYMENT_REF"
{ "ticket": "a1b2c3...", "expiresIn": 300 }PAYMENT_REF is the contract's shortId, the value in the checkout URL path from step 1. Not an internal id or an order reference of your own. A mismatch makes later calls fail. More about tickets in Authentication.
| Response | Meaning |
|---|---|
200 ticket | Issued, even while consent is still pending. Checkout checks it again live. |
400 consent_rejected | The buyer declined your organization. A fresh consent link, OTP verification or checkout login can reverse it, then retry. |
400 invalid_grant | The buyer is not linked to this client. Register or link them first. |
3. Send the buyer to checkout
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 the ticket as its own ticket query parameter to the URL from step 1 and redirect the buyer's browser there. Do this from your backend. Waffy's checkout page exchanges the ticket for a real token itself, so your integration ends at the redirect.
const url = checkoutUrl + "&ticket=" + encodeURIComponent(ticket); window.location.href = url; // or Flutter: launchUrl(Uri.parse(url))
Option B: Waffy SDK (Web, Android, iOS, React Native)
The SDK shows the checkout in a modal, popup or iframe. Include the script from https://sdk.waffyapp.com/v2/waffy-payment-display.min.js in your page, then call:
WaffyPaymentDisplay.show({
paymentUrl: "https://external.waffyapp.com/external/UXFmUCaG?client_id=...",
userToken: CUSTOMER_TOKEN,
mode: "modal" // or "popup" or "iframe"
});Check the SDK token with Waffy
Android, iOS and React Native use the platform equivalents of the same SDK. Your integration team receives the package references during onboarding.
Legacy: userTokenUrl (deprecated)
userTokenUrl parameter holding the buyer's legacy customer_token instead of a ticket. That keeps working while they move, but new integrations should use the ticket.Handle the PAID event
When the buyer completes payment, you will receive:
{
"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
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.
# 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 and process the event asynchronously.
Respond 200 first, process second
Webhook status values
| Status | Meaning |
|---|---|
PAID | Buyer paid. Funds are in escrow. Move to delivery. |
CASHOUT_IN_PROGRESS | Funds are being released to parties. No action needed. |
COMPLETED | Contract is fully settled. All parties have been paid out. |