Set up the contract
Three API calls in order — create parent, add milestone, add parties. Once parties land, the contract is payable.
- 1Auth
- 2Users
- 3Contract
- 4Payment
- 5Settle
Token for these calls
403. The contract is attributed to the client you signed in with, so use that same client for the checkout step later.A Waffy contract has two layers:
- •Parent (
COMPLEX_CONTRACT) — the container. Holds no money. - •Milestone (
MILESTONE_CONTRACT) — the actual escrow transaction. Holds the money.
Step 6 — Create the parent contract
The parent is a container. itemPrice: 0 is intentional — the amount lives on the milestone. senderRole: BROKER means the org admin is the one creating the contract, not the seller.
curl -s "$WAFFY_BASE_URL/api/external/contracts" \
-H "Authorization: Bearer $CLIENT_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "COMPLEX_CONTRACT",
"senderRole": "BROKER",
"contractClassification": "BASIC",
"currency": "SAR",
"itemPrice": 0,
"itemDetail": {
"title": "iPhone 15 Pro",
"description": "Used iPhone 15 Pro, 256GB, excellent condition"
},
"returnPolicy": "NO_RETURN",
"returnFeePayee": "PROVIDER",
"waffyTermsAccepted": true,
"isParent": true
}'Response:
{
"data": {
"id": "507f1f77bcf86cd799439011",
"type": "COMPLEX_CONTRACT",
"senderRole": "BROKER",
"status": "CREATED"
}
}Save data.id as complex_contract_id.
Step 7 — Add the milestone
This is where the amount, conditions, and deadline live. Payment methods, inspection, and delivery settings are configured at org level during onboarding — do not pass them here.
curl -s -X PATCH "$WAFFY_BASE_URL/api/external/contracts/$CONTRACT_ID/milestones" \
-H "Authorization: Bearer $CLIENT_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"milestones": [{
"type": "MILESTONE_CONTRACT",
"senderRole": "BROKER",
"itemDetail": {
"title": "iPhone 15 Pro",
"description": "Used iPhone 15 Pro, 256GB, excellent condition"
},
"itemPrice": 5000,
"currency": "SAR",
"returnPolicy": "NO_RETURN",
"returnFeePayee": "PROVIDER",
"deadLine": "2026-07-01T23:59:59Z",
"waffyTermsAccepted": true
}]
}'Response:
{
"data": {
"parentContractId": "507f1f77bcf86cd799439011",
"milestones": [{
"id": "607f1f77bcf86cd799439022",
"itemPrice": 5000,
"fee": 200,
"feePercentage": 0.04,
"status": "CREATED"
}]
}
}Save milestones[0].id as milestone_id. fee is auto-calculated from your org's agreement — deducted from your org wallet, not from party amounts. Key all downstream balance logic off itemPrice, not a derived total.
Do not pass paymentMethods, isDeliverable, or isInspectable
These are org-level settings configured during onboarding. Passing them per-milestone is rejected.
Step 8 — Add parties to the milestone
Use phone numbers — Waffy resolves the user from phone. Total CUSTOMER amounts must equal total BROKER + PROVIDER amounts on each milestone.
curl -s -X PATCH "$WAFFY_BASE_URL/api/external/contracts/$CONTRACT_ID/parties" \
-H "Authorization: Bearer $CLIENT_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"mileStonesParties\": {
\"$MILESTONE_ID\": [
{
\"phoneNumber\": \"{{buyer_phone}}\",
\"role\": \"CUSTOMER\",
\"amount\": 5000,
\"arbitrator\": false,
\"isSender\": true
},
{
\"phoneNumber\": \"$WAFFY_ADMIN_PHONE\",
\"role\": \"BROKER\",
\"amount\": 4800,
\"arbitrator\": true,
\"isSender\": true
},
{
\"phoneNumber\": \"{{seller_phone}}\",
\"role\": \"PROVIDER\",
\"amount\": 200
}
]
}
}"Party rules — always, without exception
- •
BROKERis the org admin — alwaysarbitrator: trueandisSender: true - •
CUSTOMERcarriesisSender: true - •
PROVIDERomitsisSenderentirely — it defaults false. Sending it is rejected.
invitationAllowed — check your org config
If invitationAllowed: true — parties receive an invitation and must accept in the Waffy app before payment can proceed.
If invitationAllowed: false — parties are auto-joined immediately after this call, no manual step required.
Your account manager confirms which applies to your org during onboarding.
Once parties land, the contract is automatically payable — there is no separate publish call for API integrations. Status moves CREATED → WAITING_FOR_PAYMENT automatically.
(The SUBMITTED state with an explicit publish step exists only in the Waffy B2B portal path — API integrators never hit it.)