Hold A Payout Until Confirmed

Hold merchant payout funds in escrow until a confirmation event arrives, then release the merchant share with a wait_event step.


A two-leg payout where the second leg is held until an external system confirms delivery. The platform sends the gross amount to an escrow wallet first, then waits for a delivery.confirmed event before releasing the merchant share.

Problem

Your marketplace wants to pay merchants only after the buyer has confirmed receipt of goods (or after a fraud check passes). Until that confirmation arrives, the merchant share should be held; if the confirmation never arrives, the workflow should fail loudly so an operator can investigate.

Workflow definition

version: 1
name: "marketplace-escrow"
description: "Hold the merchant share until the buyer confirms delivery"
steps:
    - name: "send_to_escrow"
      send_money:
        source:
            type: "wallet"
            account: "${input.platform_wallet}"
        destination:
            type: "wallet"
            account: "${input.escrow_wallet}"
            account_name: "Escrow Holding"
            bic: "PAEYPHM2XXX"
        provider: "paymongo"
        amount: "${input.gross_amount}"
        currency: "PHP"
        notes: "Order ${input.order_id} (escrow)"
    - name: "wait_for_confirmation"
      wait_event:
        event_name: "delivery.confirmed"
        timeout: "P3D"
        on_timeout: "fail"
        payload_schema:
          type: "object"
          required: ["order_id"]
          properties:
            order_id:
              type: "string"
    - name: "release_to_merchant"
      send_money:
        source:
            type: "wallet"
            account: "${input.escrow_wallet}"
        destination:
            type: "wallet"
            account: "${input.merchant_wallet}"
            account_name: "${input.merchant_name}"
            bic: "PAEYPHM2XXX"
        provider: "paymongo"
        amount: "${input.merchant_share}"
        currency: "PHP"
        notes: "Order ${steps.wait_for_confirmation.output.event_payload.order_id} (release)"

The wait_event step pauses the workflow for up to three days. If no delivery.confirmed event arrives in that window, on_timeout: fail ends the instance in failed status, so your ops dashboard can flag stuck escrow positions to reconcile.

The payload_schema rejects events that do not carry an order_id, which means the third step can safely reference ${steps.wait_for_confirmation.output.event_payload.order_id} without further validation.

Trigger

The workflow runs every time an order is created. Subscribe to whatever event your application publishes for new orders:

curl --request POST 'https://workflow-api.paymongo.com/v1/triggers' \
  --header 'Authorization: Basic ${YOUR_BASIC_TOKEN}' \
  --header 'Organization-Id: ${YOUR_ORG_ID}' \
  --header 'Content-Type: application/json' \
  --data '{
    "workflow_id": "wf_marketplace",
    "condition": {
      "event_name": "marketplace.order.created"
    }
  }'

Confirming an order is a separate API call against the running instance:

curl --request POST 'https://workflow-api.paymongo.com/v1/instances/${INSTANCE_ID}/events' \
  --header 'Authorization: Basic ${YOUR_BASIC_TOKEN}' \
  --header 'Organization-Id: ${YOUR_ORG_ID}' \
  --header 'Content-Type: application/json' \
  --data '{
    "event_name": "delivery.confirmed",
    "payload": {
      "order_id": "ord_abc123"
    }
  }'

To find which instance to confirm, use GET /v1/instances/waiting?event_name=delivery.confirmed.

What success looks like

A confirmed run shows three completed steps (send_to_escrow, wait_for_confirmation with event_received: true, and release_to_merchant):

{
  "data": {
    "instance_id": "inst_xyz",
    "status": "completed",
    "output": {
      "steps_executed": 3,
      "step_outputs": [
        {"status": "completed", "transfer_id": "tr_escrow"},
        {"status": "completed", "event_received": true, "event_payload": {"order_id": "ord_abc123"}},
        {"status": "completed", "transfer_id": "tr_release"}
      ]
    }
  }
}

If the timeout elapses, the instance ends in failed status with the second step's status set to timed_out. The escrow funds remain on the escrow wallet, and your ops process should reclaim or refund them manually.

Variations

  • Soft hold. Switch on_timeout: "fail" to on_timeout: "continue" if you want the workflow to release the funds anyway after the timeout. The third step runs unconditionally.
  • Different timeout. PT24H (24 hours), P7D (7 days, the maximum), or any ISO 8601 duration up to seven days.
  • Stricter payload validation. Expand payload_schema to require additional fields like confirmed_by or confirmation_method.
  • Multiple confirmation attempts. Set max_retries: 2 to allow up to three timeout windows in sequence (total wait still capped at 7 days).

See also