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
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 $USER_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 $USER_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 $USER_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.)