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.

  1. 1Auth
  2. 2Users
  3. 3Contract
  4. 4Payment
  5. 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 }
    ]
  }"
FieldValue
userId / senderIdThe client admin's own user identification ($ADMIN_ID).
cashOutAmountList[].idOne entry per recipient.
$PROVIDER_IDThe provider's own user identification (provider_id).
$ORG_ACCOUNT_IDYour 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

MistakeFix
Using the org token for contract calls or startPaymentUse the client admin token. Anything else gets 403.
Not appending the ticket to the checkout URLRequired for the raw redirect. Get it right before redirecting.
Using an internal id or your own order reference as the ticket's payment_refIt must be the contract's shortId.
senderRole: PROVIDER on milestoneMust be BROKER on parent and every milestone
Business logic keyed off totalAmountUse itemPrice everywhere (parties, cashOutAmountList)
Forgetting isSender: true on BROKERBROKER must have both isSender: true and arbitrator: true
actorRole: BROKER on ACCEPT or REJECTUse CLIENT_ADMIN with the client admin's own user identification as userId, as for SETTLE
receiverId in SETTLERemove it. It is not a field.
The admin's user identification as a recipient in cashOutAmountListUse your organization's account id (org_account_id)
cashOutAmountList doesn't add up to the payable amountMust equal it exactly
paymentMethods in milestoneRemove. Configured at org level during onboarding
isDeliverable / isInspectable in milestoneRemove. These are org-level settings, not per-milestone
isSender on PROVIDEROmit entirely. Sending it is rejected by the API
Calling ACCEPT/REJECT outside WAITING_FOR_DELIVERYOnly valid in that status. Check the milestone status before acting
Not verifying the webhook signatureAlways HMAC-SHA256 verify Waffy-Signature before processing
Slow webhook responseRespond HTTP 200 immediately, process async