Geldstuck escrow holds funds (or title) between two parties until both sides confirm. A transaction has a lifecycle of precise states and emits a webhook on every transition.

Lifecycle

Prerequisites

  • Both parties must exist as users in your tenant.
  • Both parties must have completed KYC (or a reusable KYC record).
  • The buyer must have a verified source-of-funds (for transactions above your threshold).

Step 1 - Create the transaction

curl https://api.geldstuck.com/v1/transactions/create \
  -H "x-api-key: $GELDSTUCK_PUBLIC_KEY" \
  -H "x-api-secret: $GELDSTUCK_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $ORDER_ID" \
  -d '{
    "accountType": "escrow",
    "title": "1972 Rolex Submariner",
    "amount": 1850000,
    "currency": "EUR",
    "seller": { "userId": "usr_seller_..." },
    "buyer":  { "email":  "bob@example.com" },
    "timeline": { "deliveryDays": 7, "inspectionDays": 3 }
  }'
You can pass a counterparty by userId (existing user) or email (auto-invited). If the email doesn’t resolve to a user, Geldstuck creates a pending user and sends an invitation.

Step 2 - Invite the counterparty

The create call returns an inviteToken - a signed URL that lets the counterparty accept without logging in first.
{
  "id": "tx_01HX3ZM...",
  "status": "invited",
  "inviteToken": "ey...",
  "inviteUrl": "https://app.geldstuck.com/accept/ey...",
  ...
}
Embed inviteUrl in your notification (email, SMS, in-app). The counterparty lands on the Geldstuck-hosted acceptance page - no integration needed from your side.

Step 3 - Wait for acceptance

When the counterparty clicks through and agrees, you get:
{
  "id": "evt_01HX3ZN...",
  "type": "transaction.accepted",
  "tenantId": "tnt_01HX3Z8MQW...",
  "data": {
    "transactionId": "tx_01HX3ZM...",
    "acceptedBy": "usr_01HX3ZO...",
    "acceptedAt": "2026-04-22T11:02:44.000Z"
  }
}

Step 4 - Fund the escrow

Once accepted, the buyer deposits funds. If you’ve enabled the Payments module, this can be a card or bank transfer through Geldstuck. Otherwise, you settle off-platform and call:
POST /v1/transactions/tx_01HX3ZM.../mark-funded
{ "reference": "wire-ref-abc123" }
This fires transaction.status.changed with status: "funded".

Step 5 - Release or refund

Either party can request release/refund through the hosted UI, or you can drive it programmatically:
POST /v1/transactions/tx_01HX3ZM.../release
POST /v1/transactions/tx_01HX3ZM.../refund
Each fires a corresponding transaction.status.changed.

Fetching the current transaction

For a logged-in user, resolve the transaction they’re currently party to:
GET /v1/transactions/current
Returns the first transaction where userId matches either buyer.id or seller.id.

Disputes

If either party disputes, the transaction enters disputed. Your ops team (or ours, if you’ve enabled managed disputes) reviews evidence and issues a decision. Disputes emit:
  • transaction.disputed when opened
  • transaction.dispute.updated on every evidence upload
  • transaction.status.changed with released or refunded when resolved
Subscribe to transaction.* at your webhook endpoint - the wildcard keeps you covered as new sub-events are added.