Build a Payment · Step 5 of 5
Accept + settle
Confirm delivery, then distribute funds with cashOutAmountList. Two calls, one sum-to-the-total rule, and you're done.
- 1Auth
- 2Users
- 3Contract
- 4Payment
- 5Settle
Token and ids for these calls
All three calls use the client admin token as the bearer (Authentication, step 2).
$ADMIN_ID is the client admin's own user identification, the admin_id in the Postman collection, and actorRole is CLIENT_ADMIN on every one of them.Accept delivery
After payment, the milestone is in WAITING_FOR_DELIVERY. The admin confirms delivery succeeded.
bash
curl -s "$API_BASE_URL/contract-actions/external" \
-H "Authorization: Bearer $CLIENT_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"contractId\": \"$MILESTONE_ID\",
\"userId\": \"$ADMIN_ID\",
\"actorRole\": \"CLIENT_ADMIN\",
\"contractAction\": \"ACCEPT_CONTRACT\",
\"contractType\": \"MILESTONE_CONTRACT\"
}"Status flow after accept:
WAITING_FOR_DELIVERY
→ ACCEPT_CONTRACT
→ ITEM_INSPECTION (if org has inspection enabled)
→ RESOLVED_RELEASE_PROVIDER (ready to settle)
If inspection is not enabled for your org, the flow goes directly from accept to RESOLVED_RELEASE_PROVIDER.
Or reject delivery
If delivery failed, reject instead. Funds route back to the buyer on settlement.
bash
curl -s "$API_BASE_URL/contract-actions/external" \
-H "Authorization: Bearer $CLIENT_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"contractId\": \"$MILESTONE_ID\",
\"userId\": \"$ADMIN_ID\",
\"actorRole\": \"CLIENT_ADMIN\",
\"contractAction\": \"REJECT_CONTRACT\",
\"contractType\": \"MILESTONE_CONTRACT\"
}"Status flow after reject:
WAITING_FOR_DELIVERY
→ REJECT_CONTRACT
→ RESOLVED_REFUND_CUSTOMER (ready to settle → cash out to buyer)
Settle
Distributes funds. The amountDue values in cashOutAmountList must add up to the milestone's total payable amount exactly. There is no receiverId field.
bash
curl -s "$API_BASE_URL/contract-actions/external" \
-H "Authorization: Bearer $CLIENT_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"contractId\": \"$MILESTONE_ID\",
\"userId\": $ADMIN_ID,
\"actorRole\": \"CLIENT_ADMIN\",
\"senderId\": $ADMIN_ID,
\"contractType\": \"MILESTONE_CONTRACT\",
\"contractAction\": \"SETTLE_CONTRACT\",
\"cashOutAmountList\": [
{ \"id\": $PROVIDER_ID, \"amountDue\": 4800 },
{ \"id\": $ORG_ACCOUNT_ID, \"amountDue\": 200 }
]
}"| Field | Value |
|---|---|
userId / senderId | The client admin's own user identification ($ADMIN_ID). |
cashOutAmountList[].id | One entry per recipient. |
$PROVIDER_ID | The provider's own user identification (provider_id). |
$ORG_ACCOUNT_ID | Your organization's account identifier (the same id that appears in contract.parties), not the admin's user identification. It is prefilled as org_account_id in the environment you download from the dashboard. |
Fee deduction rules
Waffy's fee is deducted from your org's cut of the transaction, not from the seller's or buyer's payout. If your cut is 0, Waffy applies a negative balance deducted from future revenue.
Walkthrough complete
You've built a one-milestone escrow end to end. The next step is picking the integration pattern that matches what your product needs. Every pattern reuses the same steps you just built, only the
parties and cashOutAmountList blocks change.Common mistakes
| Mistake | Fix |
|---|---|
Using the org token for contract calls or startPayment | Use the client admin token. Anything else gets 403. |
Not appending the ticket to the checkout URL | Required for the raw redirect. Get it right before redirecting. |
Using an internal id or your own order reference as the ticket's payment_ref | It must be the contract's shortId. |
senderRole: PROVIDER on milestone | Must be BROKER on parent and every milestone |
Business logic keyed off totalAmount | Use itemPrice everywhere (parties, cashOutAmountList) |
Forgetting isSender: true on BROKER | BROKER must have both isSender: true and arbitrator: true |
actorRole: BROKER on ACCEPT or REJECT | Use CLIENT_ADMIN with the client admin's own user identification as userId, as for SETTLE |
receiverId in SETTLE | Remove it. It is not a field. |
The admin's user identification as a recipient in cashOutAmountList | Use your organization's account id (org_account_id) |
cashOutAmountList doesn't add up to the payable amount | Must equal it exactly |
paymentMethods in milestone | Remove. Configured at org level during onboarding |
isDeliverable / isInspectable in milestone | Remove. These are org-level settings, not per-milestone |
isSender on PROVIDER | Omit entirely. Sending it is rejected by the API |
Calling ACCEPT/REJECT outside WAITING_FOR_DELIVERY | Only valid in that status. Check the milestone status before acting |
| Not verifying the webhook signature | Always HMAC-SHA256 verify Waffy-Signature before processing |
| Slow webhook response | Respond HTTP 200 immediately, process async |