Getting Started · Step 3 of 5

Set up the contract

Three API calls in order — create parent, add milestone, add parties. Once parties land, the contract is payable.

  1. 1Auth
  2. 2Users
  3. 3Contract
  4. 4Payment
  5. 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.

bash
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:

json
{
  "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.

bash
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:

json
{
  "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.

bash
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

  • BROKER is the org admin — always arbitrator: true and isSender: true
  • CUSTOMER carries isSender: true
  • PROVIDER omits isSender entirely — 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.)