UnifiWallet Payments

API reference

The endpoints you will actually call: URL, parameters, response, errors, and a working request in the language you are writing.

Click an operation to open its URL, parameters, response, errors and a working sample in Node.js, TypeScript, Python, Go, PHP or curl. The same signed call helper is what every sample uses — copy it once.

Conventions

  • Base URL https://api.payment.unifiwallet.com.
  • Every request is signed, except GET /v1/status.
  • Every POST that creates or moves work needs an Idempotency-Key.
  • Money is always a decimal string. Never a float.
  • Timestamps are ISO 8601 in UTC.
  • Lists return { object, data, has_more, next_cursor }. Page with starting_after.
  • Unknown fields in a response are added over time. Ignore what you do not recognise.

Common errors

These can arrive on any authenticated route. Each operation lists only the errors that belong to it.

StatusCodeWhen
401missing_signatureOne of the signing headers is absent. See Authentication.
401invalid_signatureThe HMAC does not match the bytes you sent. Almost always the body.
403insufficient_scopeThe key is real but was not granted the scope this route needs.
429rate_limit_exceededRead Retry-After and back off. Do not retry immediately.
{
  "error": {
    "type": "authentication_error",
    "code": "invalid_signature",
    "message": "The signature does not match the request.",
    "request_id": "req_9f3aKd2mQx7b",
    "doc_url": "https://payment.unifiwallet.com/docs/errors#invalid_signature"
  }
}

Payments

An intent to be paid a specific amount, with the fee, rate and destination frozen at creation.

  1. POST /v1/payments Create a payment
  2. GET /v1/payments List payments
  3. GET /v1/payments/{id} Retrieve a payment
  4. GET /v1/payments/{id}/events Payment status history
  5. POST /v1/payments/{id}/cancel Cancel a payment
  6. POST /v1/payments/{id}/expire Expire a payment early
  7. POST /v1/payments/{id}/capture Capture an authorization
  8. POST /v1/payments/{id}/void Void an authorization
POST /v1/payments Create a payment

Creates a payment and returns a checkout URL. Everything that decides where money goes — destination, asset, fee plan, tax, capture method — is snapshotted here and never recalculated.

Signed request. Scope payments:write. Idempotency key required.

URL

https://api.payment.unifiwallet.com/v1/payments

Parameters

Headers

NameTypeRequiredDescription
Idempotency-KeystringYesRequired on every POST. Reuse it on retry; we return the original response.

Body

NameTypeRequiredDescription
amountstringYesDecimal string in the presentment currency. Never a float.
currencystringYesFiat code or asset symbol, 2–10 characters.
merchant_order_idstring—Your reference. Unique per account, and how you search later.
descriptionstring—Shown to the buyer on checkout. Maximum 500 characters.
line_itemsarray—Optional breakdown. Each item needs name and unit_amount; quantity defaults to 1.
line_items[].namestringYesLine title.
line_items[].quantityinteger—Integer ≥ 1. Defaults to 1.
line_items[].unit_amountstringYesDecimal string per unit.
customer.referencestring—Your customer id. Returned on the payment and every webhook about it.
customer.emailstring—Used only to send a receipt, and encrypted at rest.
customer.email_receiptboolean—Whether to email a receipt after the payment completes.
customer.address.countrystring—ISO 3166-1 alpha-2. Used for tax jurisdiction.
customer.address.postal_codestring—Billing postal code, when you have it.
customer.tax_id.typestring—Tax identifier type, for example vat.
customer.tax_id.valuestring—The identifier itself.
customer.refund_addressstring—Preferred refund destination. The buyer can also set this at checkout.
tax_behaviorstring—inclusive or exclusive.
tax_rate_percentnumber—0–100. Combined with customer country for the snapshot.
settlement.destination_idstring—A destination you have already verified. Snapshotted onto this payment.
settlement.addressstring—Per-payment routing to a 0x address. Refused unless you switched this on in the panel.
settlement.assetstring—Override the settlement asset for this payment only.
settlement.chain_idinteger—Override the settlement chain for this payment only.
capture_methodstring—automatic (default) or manual. Manual holds the funds in escrow until you capture; it needs an escrow contract on your settlement chain.
hold_window_hoursinteger—For a manual payment: how long the funds stay held before the buyer may reclaim them. Defaults to your account setting, capped by the platform maximum.
rate_modestring—fixed_rate or floating_rate.
rate_lock_secondsinteger—300–1800. How long a fixed rate is held.
return_urlstring—HTTPS URL on a domain you have verified. A hint, not proof of payment.
cancel_urlstring—HTTPS URL on a verified domain, for a buyer who backs out.
failure_urlstring—HTTPS URL on a verified domain, if you want a dedicated failure page.
expires_ininteger—Seconds until the payment expires. 60–86400.
fee_payerstring—merchant (default) or buyer. Grossing up is done for you.
gas_payerstring—buyer (default) or merchant. Who carries the network fee — added to the buyer's total, or taken from your settlement. Defaults to your account setting.
discountsarray—Reductions applied before tax. Each entry takes a coupon or a flat amount.
payer_address_hintstring—The wallet you expect to pay, when you already know it.
metadataobject—Yours. Returned on the payment and every webhook about it.

Errors

StatusCodeWhen
400below_minimum_paymentUnder your account floor. Network fees would cost more than the sale.
400no_settlement_destinationRegister and verify a wallet first.
400settlement_destination_not_verifiedSign the verification message with that wallet.
400destination_in_cooling_periodThe destination is still cooling. The response says until when.
400domain_not_verifiedreturn_url, cancel_url or failure_url is on a domain you have not proved you own.
400manual_capture_unavailableHolds are not switched on for this deployment, or no escrow contract is deployed on your settlement chain.
403live_not_enabledAdd your business details before taking live payments.
409idempotency_key_reuseSame Idempotency-Key, different body. Use a new key for a different request.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const payment = await call('POST', '/v1/payments', {
    "amount": "129.00",
    "currency": "USD",
    "merchant_order_id": "order_2208",
    "description": "Annual licence",
    "line_items": [
      {
        "name": "Annual licence",
        "quantity": 1,
        "unit_amount": "129.00"
      }
    ],
    "customer": {
      "reference": "cus_88213",
      "email": "buyer@example.com",
      "address": {
        "country": "DE"
      }
    },
    "tax_behavior": "exclusive",
    "tax_rate_percent": 19,
    "return_url": "https://shop.example.com/thanks",
    "cancel_url": "https://shop.example.com/basket",
    "expires_in": 1800,
    "metadata": {
      "campaign": "spring"
    }
  });

Response 201

{
  "id": "pay_3nQ8xK2mLp4R",
  "object": "payment",
  "mode": "test",
  "status": "created",
  "amount": "129.00",
  "currency": "USD",
  "merchant_order_id": "order_2208",
  "description": "Annual licence",
  "metadata": {
    "campaign": "spring"
  },
  "customer": {
    "reference": "cus_88213",
    "country": "DE"
  },
  "snapshot": {
    "fee_plan_version": "plan_std_v3",
    "fee_bps": 100,
    "capture_method": "automatic",
    "rate_mode": "fixed_rate",
    "settlement": {
      "path": "direct",
      "destination_id": "dst_9k2Qm",
      "resolved_address": "0x8f2a4c7b91e0d3a6",
      "asset": "USDC",
      "chain_id": 8453
    },
    "confirmations_required": 20,
    "tax": {
      "behavior": "exclusive",
      "total": "24.51",
      "jurisdiction": "DE"
    },
    "totals": {
      "subtotal": "129.00",
      "tax": "24.51",
      "total": "153.51"
    }
  },
  "expires_at": "2026-09-04T11:30:00Z",
  "created_at": "2026-09-04T11:00:00Z",
  "checkout_url": "https://checkout.payment.unifiwallet.com/c/ct_9f3aKd2m",
  "deeplink": "unifiwallet://pay/ct_9f3aKd2m",
  "client_secret": "pcs_8f2a4c7b91e0"
}
GET /v1/payments List payments

Newest first. Filter by status or your order id. Page with starting_after.

Signed request. Scope payments:read.

URL

https://api.payment.unifiwallet.com/v1/payments?status=completed&limit=50

Parameters

Query

NameTypeRequiredDescription
statusstring—Filter to one status: created, settling, completed, expired, cancelled, and the rest of the lifecycle.
merchant_order_idstring—Your reference. Returns the matching payment, if any.
limitinteger—Page size. Default 50, maximum 100.
starting_afterstring—Cursor from the previous page — the list’s next_cursor.

Errors

No route-specific errors beyond the common ones above.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const payments = await call('GET', '/v1/payments?status=completed&limit=50');

Response 200

{
  "object": "list",
  "data": [
    {
      "id": "pay_3nQ8xK2mLp4R",
      "object": "payment",
      "mode": "test",
      "status": "completed",
      "amount": "129.00",
      "currency": "USD",
      "merchant_order_id": "order_2208",
      "description": "Annual licence",
      "metadata": {
        "campaign": "spring"
      },
      "customer": {
        "reference": "cus_88213",
        "country": "DE"
      },
      "snapshot": {
        "fee_plan_version": "plan_std_v3",
        "fee_bps": 100,
        "capture_method": "automatic",
        "rate_mode": "fixed_rate",
        "settlement": {
          "path": "direct",
          "destination_id": "dst_9k2Qm",
          "resolved_address": "0x8f2a4c7b91e0d3a6",
          "asset": "USDC",
          "chain_id": 8453
        },
        "confirmations_required": 20,
        "tax": {
          "behavior": "exclusive",
          "total": "24.51",
          "jurisdiction": "DE"
        },
        "totals": {
          "subtotal": "129.00",
          "tax": "24.51",
          "total": "153.51"
        }
      },
      "expires_at": "2026-09-04T11:30:00Z",
      "created_at": "2026-09-04T11:00:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
GET /v1/payments/{id} Retrieve a payment

One payment, with the terms that applied and the settlement if money has arrived. Always authoritative — prefer this over the return URL.

Signed request. Scope payments:read.

URL

https://api.payment.unifiwallet.com/v1/payments/pay_3nQ8xK2mLp4R

Parameters

Path

NameTypeRequiredDescription
idstringYesThe payment id, pay_…

Errors

StatusCodeWhen
404payment_not_foundNo such payment, or it belongs to another account. We do not distinguish.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const payment = await call('GET', '/v1/payments/pay_3nQ8xK2mLp4R');

Response 200

{
  "id": "pay_3nQ8xK2mLp4R",
  "object": "payment",
  "mode": "test",
  "status": "completed",
  "amount": "129.00",
  "currency": "USD",
  "merchant_order_id": "order_2208",
  "description": "Annual licence",
  "metadata": {
    "campaign": "spring"
  },
  "customer": {
    "reference": "cus_88213",
    "country": "DE"
  },
  "snapshot": {
    "fee_plan_version": "plan_std_v3",
    "fee_bps": 100,
    "capture_method": "automatic",
    "rate_mode": "fixed_rate",
    "settlement": {
      "path": "direct",
      "destination_id": "dst_9k2Qm",
      "resolved_address": "0x8f2a4c7b91e0d3a6",
      "asset": "USDC",
      "chain_id": 8453
    },
    "confirmations_required": 20,
    "tax": {
      "behavior": "exclusive",
      "total": "24.51",
      "jurisdiction": "DE"
    },
    "totals": {
      "subtotal": "129.00",
      "tax": "24.51",
      "total": "153.51"
    }
  },
  "expires_at": "2026-09-04T11:30:00Z",
  "created_at": "2026-09-04T11:00:00Z",
  "settlement": {
    "id": "set_2hN6qR4bVk8J",
    "path": "direct",
    "tx_hash": "0x91e0d3a68f2a4c7b",
    "block_number": 22180412,
    "gross": "153.51",
    "fee": "1.54",
    "net": "151.97",
    "asset": "USDC",
    "chain_id": 8453,
    "confirmations": 20
  }
}
GET /v1/payments/{id}/events Payment status history

Every status the payment has been through, with reasons. The fastest answer to why a payment is stuck.

Signed request. Scope payments:read.

URL

https://api.payment.unifiwallet.com/v1/payments/pay_3nQ8xK2mLp4R/events

Parameters

Path

NameTypeRequiredDescription
idstringYesThe payment id, pay_…

Errors

StatusCodeWhen
404payment_not_foundNo such payment, or it belongs to another account.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const events = await call('GET', '/v1/payments/pay_3nQ8xK2mLp4R/events');

Response 200

{
  "object": "list",
  "data": [
    {
      "status": "created",
      "at": "2026-09-04T11:00:00Z",
      "reason": null
    },
    {
      "status": "settling",
      "at": "2026-09-04T11:08:12Z",
      "reason": "buyer_submitted"
    },
    {
      "status": "completed",
      "at": "2026-09-04T11:14:22Z",
      "reason": "confirmations_reached"
    }
  ]
}
POST /v1/payments/{id}/cancel Cancel a payment

Cancels a payment nobody has paid. One that has money on chain cannot be cancelled — refund it instead.

Signed request. Scope payments:write. Idempotency key required.

URL

https://api.payment.unifiwallet.com/v1/payments/pay_3nQ8xK2mLp4R/cancel

Parameters

Path

NameTypeRequiredDescription
idstringYesThe payment id, pay_…

Errors

StatusCodeWhen
404payment_not_foundNo such payment, or it belongs to another account.
409not_cancellableThe payment is not in created or awaiting_payment. Refund it if money has moved.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const payment = await call('POST', '/v1/payments/pay_3nQ8xK2mLp4R/cancel');

Response 200

{
  "id": "pay_3nQ8xK2mLp4R",
  "object": "payment",
  "mode": "test",
  "status": "cancelled",
  "amount": "129.00",
  "currency": "USD",
  "merchant_order_id": "order_2208",
  "description": "Annual licence",
  "metadata": {
    "campaign": "spring"
  },
  "customer": {
    "reference": "cus_88213",
    "country": "DE"
  },
  "snapshot": {
    "fee_plan_version": "plan_std_v3",
    "fee_bps": 100,
    "capture_method": "automatic",
    "rate_mode": "fixed_rate",
    "settlement": {
      "path": "direct",
      "destination_id": "dst_9k2Qm",
      "resolved_address": "0x8f2a4c7b91e0d3a6",
      "asset": "USDC",
      "chain_id": 8453
    },
    "confirmations_required": 20,
    "tax": {
      "behavior": "exclusive",
      "total": "24.51",
      "jurisdiction": "DE"
    },
    "totals": {
      "subtotal": "129.00",
      "tax": "24.51",
      "total": "153.51"
    }
  },
  "expires_at": "2026-09-04T11:30:00Z",
  "created_at": "2026-09-04T11:00:00Z"
}
POST /v1/payments/{id}/expire Expire a payment early

Ends the payment before expires_at. Use this when the buyer abandons the basket so the checkout link dies immediately.

Signed request. Scope payments:write. Idempotency key required.

URL

https://api.payment.unifiwallet.com/v1/payments/pay_3nQ8xK2mLp4R/expire

Parameters

Path

NameTypeRequiredDescription
idstringYesThe payment id, pay_…

Errors

StatusCodeWhen
404payment_not_foundNo such payment, or it belongs to another account.
409not_expirableThe payment is not waiting for a buyer.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const payment = await call('POST', '/v1/payments/pay_3nQ8xK2mLp4R/expire');

Response 200

{
  "id": "pay_3nQ8xK2mLp4R",
  "object": "payment",
  "mode": "test",
  "status": "expired",
  "amount": "129.00",
  "currency": "USD",
  "merchant_order_id": "order_2208",
  "description": "Annual licence",
  "metadata": {
    "campaign": "spring"
  },
  "customer": {
    "reference": "cus_88213",
    "country": "DE"
  },
  "snapshot": {
    "fee_plan_version": "plan_std_v3",
    "fee_bps": 100,
    "capture_method": "automatic",
    "rate_mode": "fixed_rate",
    "settlement": {
      "path": "direct",
      "destination_id": "dst_9k2Qm",
      "resolved_address": "0x8f2a4c7b91e0d3a6",
      "asset": "USDC",
      "chain_id": 8453
    },
    "confirmations_required": 20,
    "tax": {
      "behavior": "exclusive",
      "total": "24.51",
      "jurisdiction": "DE"
    },
    "totals": {
      "subtotal": "129.00",
      "tax": "24.51",
      "total": "153.51"
    }
  },
  "expires_at": "2026-09-04T11:30:00Z",
  "created_at": "2026-09-04T11:00:00Z"
}
POST /v1/payments/{id}/capture Capture an authorization

Charges a payment whose funds are held, in full or in part. Returns 202: the transaction is submitted and the capture is recorded when the chain confirms it, which is what payment.captured tells you. A partial capture leaves the payment authorized with the remainder still held. Bearer tokens are refused.

Signed request. Scope payments:capture. Idempotency key required. Bearer tokens are refused.

URL

https://api.payment.unifiwallet.com/v1/payments/pay_3nQ8xK2mLp4R/capture

Parameters

Path

NameTypeRequiredDescription
idstringYesThe payment id, pay_…

Headers

NameTypeRequiredDescription
Idempotency-KeystringYesRequired. Reuse it on retry.

Body

NameTypeRequiredDescription
amountstring—Decimal string to capture. Omit for the full authorized amount.

Errors

StatusCodeWhen
400escrow_unavailableHolds are not switched on for this deployment.
400invalid_capture_amountMore than is still held. Check amount_capturable.
400capture_failedThe transaction could not be submitted. Nothing moved; try again.
403signature_requiredA bearer token was used. Sign the request.
409not_authorizedOnly a payment holding funds can be captured.
409authorization_expiredThe hold lapsed and the buyer may reclaim the funds. Ask them to pay again.
409escrow_operation_in_flightA capture or void is still confirming. Wait — a second one could charge the buyer twice.
409missing_authorizationNo escrow authorization is recorded for this payment.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const payment = await call('POST', '/v1/payments/pay_3nQ8xK2mLp4R/capture', {
    "amount": "80.00"
  });

Response 202

{
  "id": "pay_3nQ8xK2mLp4R",
  "object": "payment",
  "mode": "test",
  "status": "authorized",
  "amount": "129.00",
  "currency": "USD",
  "merchant_order_id": "order_2208",
  "description": "Annual licence",
  "metadata": {
    "campaign": "spring"
  },
  "customer": {
    "reference": "cus_88213",
    "country": "DE"
  },
  "snapshot": {
    "fee_plan_version": "plan_std_v3",
    "fee_bps": 100,
    "capture_method": "automatic",
    "rate_mode": "fixed_rate",
    "settlement": {
      "path": "direct",
      "destination_id": "dst_9k2Qm",
      "resolved_address": "0x8f2a4c7b91e0d3a6",
      "asset": "USDC",
      "chain_id": 8453
    },
    "confirmations_required": 20,
    "tax": {
      "behavior": "exclusive",
      "total": "24.51",
      "jurisdiction": "DE"
    },
    "totals": {
      "subtotal": "129.00",
      "tax": "24.51",
      "total": "153.51"
    }
  },
  "expires_at": "2026-09-04T11:30:00Z",
  "created_at": "2026-09-04T11:00:00Z",
  "amount_captured": "80.00",
  "amount_capturable": "49.00",
  "escrow_operation": {
    "kind": "capture",
    "tx_hash": "0x7b41c2e8a9f3",
    "amount_base_units": "80000000",
    "submitted_at": "2026-09-16T09:14:02Z"
  },
  "capture": {
    "amount": "80.00",
    "state": "submitted"
  }
}
POST /v1/payments/{id}/void Void an authorization

Releases held funds back to the buyer without charging. Costs you a network fee to send and owes no refund, which is why cancelling a held order is cheaper than refunding a settled one. Returns 202 and is confirmed by the chain. Bearer tokens are refused.

Signed request. Scope payments:capture. Idempotency key required. Bearer tokens are refused.

URL

https://api.payment.unifiwallet.com/v1/payments/pay_3nQ8xK2mLp4R/void

Parameters

Path

NameTypeRequiredDescription
idstringYesThe payment id, pay_…

Headers

NameTypeRequiredDescription
Idempotency-KeystringYesRequired. Reuse it on retry.

Errors

StatusCodeWhen
400escrow_unavailableHolds are not switched on for this deployment.
400void_failedThe transaction could not be submitted. Nothing moved; try again.
409not_authorizedOnly a payment holding funds can be released.
409escrow_operation_in_flightA capture or void is still confirming.
409missing_authorizationNo escrow authorization is recorded for this payment.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const payment = await call('POST', '/v1/payments/pay_3nQ8xK2mLp4R/void');

Response 202

{
  "id": "pay_3nQ8xK2mLp4R",
  "object": "payment",
  "mode": "test",
  "status": "authorized",
  "amount": "129.00",
  "currency": "USD",
  "merchant_order_id": "order_2208",
  "description": "Annual licence",
  "metadata": {
    "campaign": "spring"
  },
  "customer": {
    "reference": "cus_88213",
    "country": "DE"
  },
  "snapshot": {
    "fee_plan_version": "plan_std_v3",
    "fee_bps": 100,
    "capture_method": "automatic",
    "rate_mode": "fixed_rate",
    "settlement": {
      "path": "direct",
      "destination_id": "dst_9k2Qm",
      "resolved_address": "0x8f2a4c7b91e0d3a6",
      "asset": "USDC",
      "chain_id": 8453
    },
    "confirmations_required": 20,
    "tax": {
      "behavior": "exclusive",
      "total": "24.51",
      "jurisdiction": "DE"
    },
    "totals": {
      "subtotal": "129.00",
      "tax": "24.51",
      "total": "153.51"
    }
  },
  "expires_at": "2026-09-04T11:30:00Z",
  "created_at": "2026-09-04T11:00:00Z",
  "escrow_operation": {
    "kind": "void",
    "tx_hash": "0x2d9e5a1c74b0",
    "amount_base_units": "129000000",
    "submitted_at": "2026-09-16T09:20:11Z"
  }
}

Subscriptions

A recurring charge with no stored card. Creating one charges nothing; nothing is billable until the buyer acts.

  1. POST /v1/subscriptions Create a subscription
  2. GET /v1/subscriptions/{id}/authorization Read the authorization payload
  3. POST /v1/subscriptions/{id}/authorize Submit the buyer’s signature
  4. GET /v1/subscriptions List subscriptions
  5. GET /v1/subscriptions/{id} Retrieve a subscription
  6. GET /v1/subscriptions/{id}/cycles List cycles
  7. POST /v1/subscriptions/{id}/charge Charge now
  8. POST /v1/subscriptions/{id}/cancel Cancel a subscription
POST /v1/subscriptions Create a subscription

Creates a subscription in incomplete. For auto_charge it returns the typed data the buyer must sign. For payment_link it opens the first cycle with a checkout. Bearer tokens are refused.

Signed request. Scope payments:write. Idempotency key required. Bearer tokens are refused.

URL

https://api.payment.unifiwallet.com/v1/subscriptions

Parameters

Headers

NameTypeRequiredDescription
Idempotency-KeystringYesRequired. Reuse it on retry.

Body

NameTypeRequiredDescription
amountstringYesDecimal string per cycle. Never a float.
currencystringYesISO 4217, three letters.
intervalstringYesday, week, month or year.
interval_countinteger—1–52. How many intervals per cycle. Default 1.
collection_methodstring—auto_charge (default) or payment_link.
descriptionstring—Shown to the buyer. Maximum 500 characters.
merchant_referencestring—Your reference for this subscription.
trial_endstring—ISO 8601. No charge before this instant.
ends_atstring—ISO 8601. Hard stop after this instant.
allowance_multipliernumber—1–12. Headroom above one cycle so a price rise does not need a new signature.
customer.referencestring—Your customer id.
customer.emailstring—Used for payment-link invoices and receipts.
customer.wallet_addressstring—0x address that will sign the spend permission.
customer.address.countrystring—ISO 3166-1 alpha-2.
metadataobject—Yours. Returned on the subscription and its webhooks.
settlement.destination_idstring—A destination you have already verified. Snapshotted onto this payment.
settlement.addressstring—Per-payment routing to a 0x address. Refused unless you switched this on in the panel.
settlement.assetstring—Override the settlement asset for this payment only.
settlement.chain_idinteger—Override the settlement chain for this payment only.

Errors

StatusCodeWhen
400below_minimum_paymentThe cycle amount is under your account floor.
400auto_charge_unavailableNo spend permission contract on that chain. Use payment_link.
400allowance_below_cycleThe buyer’s cap is under one cycle. Every renewal would decline.
400ends_in_the_pastends_at is already behind us.
403live_not_enabledThis account is not enabled for live payments yet.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const subscription = await call('POST', '/v1/subscriptions', {
    "amount": "19.99",
    "currency": "USD",
    "interval": "month",
    "collection_method": "auto_charge",
    "description": "Pro plan",
    "customer": {
      "reference": "cus_88213",
      "email": "buyer@example.com",
      "wallet_address": "0x4b2c91e0d3a68f2a4c7b"
    }
  });

Response 201

{
  "id": "sub_4kR9nM1wQx2P",
  "object": "subscription",
  "mode": "test",
  "status": "incomplete",
  "collection_method": "auto_charge",
  "amount": "19.99",
  "currency": "USD",
  "description": "Pro plan",
  "interval": "month",
  "interval_count": 1,
  "customer": {
    "reference": "cus_88213",
    "country": "DE"
  },
  "settlement": {
    "destination_id": "dst_9k2Qm",
    "address": "0x8f2a4c7b91e0d3a6",
    "asset": "USDC",
    "chain_id": 8453
  },
  "fee_bps": 100,
  "next_charge_at": null,
  "created_at": "2026-09-04T11:00:00Z",
  "authorization": {
    "human_summary": {
      "most_per_period": "19.99",
      "asset": "USDC",
      "period_days": 28
    },
    "typed_data": {
      "domain": {},
      "types": {},
      "message": {}
    },
    "permission": {
      "account": "0x4b2c91e0d3a68f2a4c7b",
      "spender": "0x1111111111111111111111111111111111111111",
      "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "recipient": "0x8f2a4c7b91e0d3a6",
      "fee_recipient": "0xfee0000000000000000000000000000000000001",
      "fee_bps": 100,
      "allowance": "19990000",
      "period": 2419200,
      "start": 1788000000,
      "end": 1820000000,
      "salt": "918273645",
      "chain_id": 8453
    }
  }
}
GET /v1/subscriptions/{id}/authorization Read the authorization payload

The typed data the buyer signs. Re-fetch this if the create response was discarded — closing the page is not a dead end.

Signed request. Scope payments:read.

URL

https://api.payment.unifiwallet.com/v1/subscriptions/sub_4kR9nM1wQx2P/authorization

Parameters

Path

NameTypeRequiredDescription
idstringYesThe subscription id, sub_…

Errors

StatusCodeWhen
404subscription_not_foundNo such subscription, or it belongs to another account.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const authorization = await call('GET', '/v1/subscriptions/sub_4kR9nM1wQx2P/authorization');

Response 200

{
  "human_summary": {
    "most_per_period": "19.99",
    "asset": "USDC",
    "period_days": 28
  },
  "typed_data": {
    "domain": {},
    "types": {},
    "message": {}
  },
  "permission": {
    "account": "0x4b2c91e0d3a68f2a4c7b",
    "allowance": "19990000",
    "period": 2419200
  }
}
POST /v1/subscriptions/{id}/authorize Submit the buyer’s signature

Makes an auto_charge subscription billable. Send the permission structure from create (or /authorization) and the signature from the wallet. Nothing is on chain yet.

Signed request. Scope payments:write. Bearer tokens are refused.

URL

https://api.payment.unifiwallet.com/v1/subscriptions/sub_4kR9nM1wQx2P/authorize

Parameters

Path

NameTypeRequiredDescription
idstringYesThe subscription id, sub_…

Body

NameTypeRequiredDescription
permissionobjectYesThe permission object from create or GET …/authorization. Do not invent these fields.
permission.accountstringYesBuyer’s 0x address.
permission.spenderstringYesThe spend-permission manager.
permission.tokenstringYesERC-20 the buyer is permitting.
permission.recipientstringYesYour settlement address.
permission.fee_recipientstringYesOur fee address, snapshotted at create.
permission.fee_bpsintegerYesFee in basis points, 0–500.
permission.allowancestringYesInteger base units, not a decimal.
permission.periodintegerYesPeriod length in seconds, ≥ 60.
permission.startintegerYesUnix seconds.
permission.endintegerYesUnix seconds.
permission.saltstringYesInteger as a string. Uniqueness for the permission hash.
permission.chain_idinteger—Defaults to the snapshotted chain.
signaturestringYes0x-prefixed signature from the paying wallet.

Errors

StatusCodeWhen
400invalid_signatureThe signature does not match the permission.
409subscription_overIt has been cancelled or has ended; there is nothing to authorise.
400recipient_mismatchThe signed permission pays somewhere other than your settlement address. Refused rather than reconciled.
400fee_mismatchThe signed fee is not the one this subscription was created with.
400permission_expiredThe signed window has already closed.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const subscription = await call('POST', '/v1/subscriptions/sub_4kR9nM1wQx2P/authorize', {
    "permission": {
      "account": "0x4b2c91e0d3a68f2a4c7b",
      "spender": "0x1111111111111111111111111111111111111111",
      "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "recipient": "0x8f2a4c7b91e0d3a6",
      "fee_recipient": "0xfee0000000000000000000000000000000000001",
      "fee_bps": 100,
      "allowance": "19990000",
      "period": 2419200,
      "start": 1788000000,
      "end": 1820000000,
      "salt": "918273645",
      "chain_id": 8453
    },
    "signature": "0xdeadbeef"
  });

Response 200

{
  "id": "sub_4kR9nM1wQx2P",
  "object": "subscription",
  "mode": "test",
  "status": "active",
  "collection_method": "auto_charge",
  "amount": "19.99",
  "currency": "USD",
  "description": "Pro plan",
  "interval": "month",
  "interval_count": 1,
  "customer": {
    "reference": "cus_88213",
    "country": "DE"
  },
  "settlement": {
    "destination_id": "dst_9k2Qm",
    "address": "0x8f2a4c7b91e0d3a6",
    "asset": "USDC",
    "chain_id": 8453
  },
  "fee_bps": 100,
  "next_charge_at": null,
  "created_at": "2026-09-04T11:00:00Z"
}
GET /v1/subscriptions List subscriptions

Newest first. Page with starting_after.

Signed request. Scope payments:read.

URL

https://api.payment.unifiwallet.com/v1/subscriptions?limit=50

Parameters

Query

NameTypeRequiredDescription
limitinteger—Page size. Default 50, maximum 100.
starting_afterstring—Cursor from the previous page — the list’s next_cursor.

Errors

No route-specific errors beyond the common ones above.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const subscriptions = await call('GET', '/v1/subscriptions?limit=50');

Response 200

{
  "object": "list",
  "data": [
    {
      "id": "sub_4kR9nM1wQx2P",
      "object": "subscription",
      "mode": "test",
      "status": "incomplete",
      "collection_method": "auto_charge",
      "amount": "19.99",
      "currency": "USD",
      "description": "Pro plan",
      "interval": "month",
      "interval_count": 1,
      "customer": {
        "reference": "cus_88213",
        "country": "DE"
      },
      "settlement": {
        "destination_id": "dst_9k2Qm",
        "address": "0x8f2a4c7b91e0d3a6",
        "asset": "USDC",
        "chain_id": 8453
      },
      "fee_bps": 100,
      "next_charge_at": null,
      "created_at": "2026-09-04T11:00:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
GET /v1/subscriptions/{id} Retrieve a subscription

One subscription, including the on-chain permission state when it has been signed.

Signed request. Scope payments:read.

URL

https://api.payment.unifiwallet.com/v1/subscriptions/sub_4kR9nM1wQx2P

Parameters

Path

NameTypeRequiredDescription
idstringYesThe subscription id, sub_…

Errors

StatusCodeWhen
404subscription_not_foundNo such subscription, or it belongs to another account.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const subscription = await call('GET', '/v1/subscriptions/sub_4kR9nM1wQx2P');

Response 200

{
  "id": "sub_4kR9nM1wQx2P",
  "object": "subscription",
  "mode": "test",
  "status": "active",
  "collection_method": "auto_charge",
  "amount": "19.99",
  "currency": "USD",
  "description": "Pro plan",
  "interval": "month",
  "interval_count": 1,
  "customer": {
    "reference": "cus_88213",
    "country": "DE"
  },
  "settlement": {
    "destination_id": "dst_9k2Qm",
    "address": "0x8f2a4c7b91e0d3a6",
    "asset": "USDC",
    "chain_id": 8453
  },
  "fee_bps": 100,
  "next_charge_at": null,
  "created_at": "2026-09-04T11:00:00Z"
}
GET /v1/subscriptions/{id}/cycles List cycles

Every period and what happened to it — charged, failed, skipped, open.

Signed request. Scope payments:read.

URL

https://api.payment.unifiwallet.com/v1/subscriptions/sub_4kR9nM1wQx2P/cycles

Parameters

Path

NameTypeRequiredDescription
idstringYesThe subscription id, sub_…

Errors

StatusCodeWhen
404subscription_not_foundNo such subscription, or it belongs to another account.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const cycles = await call('GET', '/v1/subscriptions/sub_4kR9nM1wQx2P/cycles');

Response 200

{
  "object": "list",
  "data": [
    {
      "id": "cyc_1",
      "object": "subscription_cycle",
      "subscription_id": "sub_4kR9nM1wQx2P",
      "sequence": 1,
      "status": "open",
      "amount": "19.99",
      "currency": "USD",
      "period_start": "2026-09-04T11:00:00Z",
      "period_end": "2026-10-02T11:00:00Z",
      "due_at": "2026-09-04T11:00:00Z"
    }
  ]
}
POST /v1/subscriptions/{id}/charge Charge now

Charges the current cycle immediately instead of waiting for next_charge_at. Bearer tokens are refused. Do not retry a cycle that is already in flight.

Signed request. Scope payments:write. Idempotency key required. Bearer tokens are refused.

URL

https://api.payment.unifiwallet.com/v1/subscriptions/sub_4kR9nM1wQx2P/charge

Parameters

Path

NameTypeRequiredDescription
idstringYesThe subscription id, sub_…

Headers

NameTypeRequiredDescription
Idempotency-KeystringYesRequired. Reuse it on retry.

Errors

StatusCodeWhen
409cycle_in_flightAlready being charged. Do not retry, or the buyer pays twice.
409not_chargeableThe subscription is incomplete, cancelled, or ended.
400not_auto_chargeThis subscription collects by payment link; there is nothing to pull against.
409cycle_in_flightThat cycle is already being charged. Do not retry, or the buyer pays twice.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const subscription = await call('POST', '/v1/subscriptions/sub_4kR9nM1wQx2P/charge');

Response 200

{
  "id": "sub_4kR9nM1wQx2P",
  "object": "subscription",
  "mode": "test",
  "status": "active",
  "collection_method": "auto_charge",
  "amount": "19.99",
  "currency": "USD",
  "description": "Pro plan",
  "interval": "month",
  "interval_count": 1,
  "customer": {
    "reference": "cus_88213",
    "country": "DE"
  },
  "settlement": {
    "destination_id": "dst_9k2Qm",
    "address": "0x8f2a4c7b91e0d3a6",
    "asset": "USDC",
    "chain_id": 8453
  },
  "fee_bps": 100,
  "next_charge_at": null,
  "created_at": "2026-09-04T11:00:00Z",
  "last_charged_at": "2026-09-04T12:00:00Z"
}
POST /v1/subscriptions/{id}/cancel Cancel a subscription

Stops charging. The buyer can also revoke the permission in their own wallet, without asking you.

Signed request. Scope payments:write.

URL

https://api.payment.unifiwallet.com/v1/subscriptions/sub_4kR9nM1wQx2P/cancel

Parameters

Path

NameTypeRequiredDescription
idstringYesThe subscription id, sub_…

Body

NameTypeRequiredDescription
at_period_endboolean—If true, finish the current period; if false or omitted, stop now.
reasonstring—Yours. Maximum 500 characters.

Errors

StatusCodeWhen
404subscription_not_foundNo such subscription, or it belongs to another account.
404subscription_not_foundNo subscription with that id on this account, in this mode.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const subscription = await call('POST', '/v1/subscriptions/sub_4kR9nM1wQx2P/cancel', {
    "at_period_end": false,
    "reason": "Closed the account"
  });

Response 200

{
  "id": "sub_4kR9nM1wQx2P",
  "object": "subscription",
  "mode": "test",
  "status": "cancelled",
  "collection_method": "auto_charge",
  "amount": "19.99",
  "currency": "USD",
  "description": "Pro plan",
  "interval": "month",
  "interval_count": 1,
  "customer": {
    "reference": "cus_88213",
    "country": "DE"
  },
  "settlement": {
    "destination_id": "dst_9k2Qm",
    "address": "0x8f2a4c7b91e0d3a6",
    "asset": "USDC",
    "chain_id": 8453
  },
  "fee_bps": 100,
  "next_charge_at": null,
  "created_at": "2026-09-04T11:00:00Z",
  "canceled_at": "2026-09-04T12:30:00Z"
}

Subscription management (buyer)

The buyer's own control over a recurring charge. No key, no signature, no account — the manage token is the authorisation, exactly like a checkout token. Give this link to your customers: a subscription only you can stop is one they will charge back instead.

  1. GET /v1/manage/{token} What the buyer sees
  2. POST /v1/manage/{token}/cancel The buyer cancels
  3. GET /v1/manage/{token}/revocation The on-chain revoke call
GET /v1/manage/{token} What the buyer sees

Price, interval, next charge and whether it can still be stopped. Deliberately nothing about your account or your customer record — this link may sit in a forwarded email.

Public — no signature.

URL

https://api.payment.unifiwallet.com/v1/manage/ms_7cQ2vB9xLp4R

Parameters

Path

NameTypeRequiredDescription
tokenstringYesThe manage token, ms_… From manage_url when the subscription was created.

Errors

StatusCodeWhen
404subscription_not_foundThe link is not valid. Says nothing more, so a guessed token reveals nothing.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const result = await call('GET', '/v1/manage/ms_7cQ2vB9xLp4R');

Response 200

{
  "id": "sub_4kR9nM1wQx2P",
  "object": "subscription_manage",
  "status": "active",
  "merchant": {
    "name": "Northwind Coffee"
  },
  "amount": "19.99",
  "currency": "USD",
  "interval": "month",
  "interval_count": 1,
  "current_period_end": "2026-10-04T12:00:00Z",
  "next_charge_at": "2026-10-04T12:00:00Z",
  "cancel_at_period_end": false,
  "cancellable": true
}
POST /v1/manage/{token}/cancel The buyer cancels

One request ends it. No confirmation step, no retention offer, no reason required. Defaults to stopping at the end of the period already paid for, which is what a person means by cancelling; immediately ends it now. You get subscription.cancel_scheduled or subscription.canceled with canceled_by: buyer, and you must act on it.

Public — no signature.

URL

https://api.payment.unifiwallet.com/v1/manage/ms_7cQ2vB9xLp4R/cancel

Parameters

Path

NameTypeRequiredDescription
tokenstringYesThe manage token, ms_…

Body

NameTypeRequiredDescription
immediatelyboolean—End it now rather than at the end of the paid period. Default false.
reasonstring—Optional, and never required — a cancellation must not be gated on answering a question.

Errors

StatusCodeWhen
404subscription_not_foundThe link is not valid.
409subscription_endedIt has already finished; there is nothing to cancel.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const result = await call('POST', '/v1/manage/ms_7cQ2vB9xLp4R/cancel', {
    "immediately": false
  });

Response 200

{
  "id": "sub_4kR9nM1wQx2P",
  "status": "active",
  "cancel_at_period_end": true,
  "current_period_end": "2026-10-04T12:00:00Z",
  "message": "This will not renew. You keep it until the current period ends."
}
GET /v1/manage/{token}/revocation The on-chain revoke call

Cancelling above stops us charging, which is a promise we keep. Revoking is the buyer's own key acting on chain: it needs neither us nor you, takes effect immediately, and is the half that requires trusting nobody. Only for auto_charge — a link subscription pre-authorises nothing.

Public — no signature.

URL

https://api.payment.unifiwallet.com/v1/manage/ms_7cQ2vB9xLp4R/revocation

Parameters

Path

NameTypeRequiredDescription
tokenstringYesThe manage token, ms_…

Errors

StatusCodeWhen
404subscription_not_foundThe link is not valid.
409no_permissionNothing was pre-authorised, so there is nothing to revoke.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const result = await call('GET', '/v1/manage/ms_7cQ2vB9xLp4R/revocation');

Response 200

{
  "manager": "0x5f2C8b1A9D4e7F03B6a1C8E2D9f4A7b3C1e8D520",
  "chain_id": 8453,
  "method": "revoke(SpendPermission)",
  "permission_hash": "0x9a1f...c3d7",
  "note": "Send this from the wallet that signed it. It takes effect immediately and needs nothing from us or from the merchant."
}

Settlements

What arrived. There is no balance and no withdraw — these records are the history of money that already landed in your wallet.

  1. GET /v1/settlements List settlements
  2. GET /v1/settlements/{id} Retrieve a settlement
  3. GET /v1/settlements/summary Volume summary
  4. GET /v1/statements Flow records
GET /v1/settlements List settlements

Arrivals, newest first. Quarantined (refused) events are hidden unless you ask for them — they are not money.

Signed request. Scope settlements:read.

URL

https://api.payment.unifiwallet.com/v1/settlements?limit=50

Parameters

Query

NameTypeRequiredDescription
payment_idstring—Limit to one payment.
statusstring—Pass quarantined to see refused events. Default excludes them.
limitinteger—Page size. Default 50, maximum 100.
starting_afterstring—Cursor from the previous page — the list’s next_cursor.

Errors

No route-specific errors beyond the common ones above.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const settlements = await call('GET', '/v1/settlements?limit=50');

Response 200

{
  "object": "list",
  "data": [
    {
      "id": "set_2hN6qR4bVk8J",
      "object": "settlement",
      "payment_id": "pay_3nQ8xK2mLp4R",
      "path": "direct",
      "chain_id": 8453,
      "tx_hash": "0x91e0d3a68f2a4c7b",
      "block_number": 22180412,
      "asset": "USDC",
      "gross": "153.51",
      "fee": "1.54",
      "gas": "0.00",
      "net": "151.97",
      "excess_returned": "0.00",
      "to_address": "0x8f2a4c7b91e0d3a6",
      "fiat_value": {
        "amount": "151.97",
        "currency": "USD"
      },
      "confirmations": 20,
      "reorged": false,
      "status": "recorded",
      "created_at": "2026-09-04T11:14:22Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
GET /v1/settlements/{id} Retrieve a settlement

One arrival: tx hash, gross, fee, net, asset, and the fiat valuation at that moment.

Signed request. Scope settlements:read.

URL

https://api.payment.unifiwallet.com/v1/settlements/set_2hN6qR4bVk8J

Parameters

Path

NameTypeRequiredDescription
idstringYesThe settlement id, set_…

Errors

StatusCodeWhen
404settlement_not_foundNo such settlement, or it belongs to another account.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const settlement = await call('GET', '/v1/settlements/set_2hN6qR4bVk8J');

Response 200

{
  "id": "set_2hN6qR4bVk8J",
  "object": "settlement",
  "payment_id": "pay_3nQ8xK2mLp4R",
  "path": "direct",
  "chain_id": 8453,
  "tx_hash": "0x91e0d3a68f2a4c7b",
  "block_number": 22180412,
  "asset": "USDC",
  "gross": "153.51",
  "fee": "1.54",
  "gas": "0.00",
  "net": "151.97",
  "excess_returned": "0.00",
  "to_address": "0x8f2a4c7b91e0d3a6",
  "fiat_value": {
    "amount": "151.97",
    "currency": "USD"
  },
  "confirmations": 20,
  "reorged": false,
  "status": "recorded",
  "created_at": "2026-09-04T11:14:22Z"
}
GET /v1/settlements/summary Volume summary

Settled volume by asset and chain since a given instant. Quarantined rows are not counted.

Signed request. Scope settlements:read.

URL

https://api.payment.unifiwallet.com/v1/settlements/summary?since=2026-08-01T00%3A00%3A00Z

Parameters

Query

NameTypeRequiredDescription
sincestring—ISO 8601. Defaults to 30 days ago.

Errors

No route-specific errors beyond the common ones above.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const summary = await call('GET', '/v1/settlements/summary?since=2026-08-01T00%3A00%3A00Z');

Response 200

{
  "object": "summary",
  "since": "2026-08-01T00:00:00Z",
  "data": [
    {
      "asset": "USDC",
      "chain_id": 8453,
      "count": 184,
      "net": "21450.20",
      "fee": "214.50"
    }
  ]
}
GET /v1/statements Flow records

Ledger entries with a fiat valuation at each timestamp, for your accountant. Capped at 1000 rows.

Signed request. Scope settlements:read.

URL

https://api.payment.unifiwallet.com/v1/statements?since=2026-08-01T00%3A00%3A00Z

Parameters

Query

NameTypeRequiredDescription
sincestring—ISO 8601. Defaults to 30 days ago.

Errors

No route-specific errors beyond the common ones above.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const statements = await call('GET', '/v1/statements?since=2026-08-01T00%3A00%3A00Z');

Response 200

{
  "object": "list",
  "data": [
    {
      "id": "led_1",
      "type": "settlement_net",
      "asset": "USDC",
      "chain_id": 8453,
      "amount": "151.97",
      "direction": "in",
      "fiat_value": {
        "amount": "151.97",
        "currency": "USD"
      },
      "created_at": "2026-09-04T11:14:22Z"
    }
  ]
}

Destinations

Wallets you control. A signed request may add one; a bearer token may not, and a new destination waits out a cooling period before it can receive. Where your money lands is not something a stolen token gets to change quietly.

  1. POST /v1/destinations Register a destination
  2. DELETE /v1/destinations/{id} Remove a destination
  3. GET /v1/destinations List destinations
  4. GET /v1/destinations/{id} Retrieve a destination
  5. GET /v1/destinations/{id}/balance On-chain balance
  6. POST /v1/destinations/{id}/verify Prove ownership
POST /v1/destinations Register a destination

Adds a wallet or Shield that money may settle to. It cannot receive until somebody signs the challenge with that wallet and the cooling period has passed — the two controls that stop a stolen key redirecting revenue. Bearer tokens are refused.

Signed request. Scope account:write. Idempotency key required. Bearer tokens are refused.

URL

https://api.payment.unifiwallet.com/v1/destinations

Parameters

Headers

NameTypeRequiredDescription
Idempotency-KeystringYesRequired. Reuse it on retry.

Body

NameTypeRequiredDescription
kindstringYesaddress or shield.
addressstring—Required when kind is address.
shield_codestring—Required when kind is shield.
chainsarray—Chain ids it may receive on. Defaults to every chain the asset exists on.
assetstring—Restrict it to one token. Omit for any accepted token.
labelstring—Your name for it, shown in the panel.

Errors

StatusCodeWhen
400invalid_addressNot an address, or the zero address.
400exchange_deposit_addressThat looks like an exchange deposit address. Arbitrary token arrivals there are commonly lost.
403signature_requiredA bearer token was used. Sign the request.
422destination_flaggedThe address did not pass screening.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const destination = await call('POST', '/v1/destinations', {
    "kind": "address",
    "address": "0x8f2a4c7b91e0d3a6",
    "chains": [
      8453
    ],
    "asset": "USDC",
    "label": "Treasury"
  });

Response 201

{
  "id": "dst_9k2Qm",
  "object": "settlement_destination",
  "kind": "address",
  "resolved_address": "0x8f2a4c7b91e0d3a6",
  "chains": [
    8453,
    1
  ],
  "asset": "USDC",
  "status": "pending_verification",
  "usable_at": "2026-09-05T09:00:00Z",
  "verification": {
    "method": "signature",
    "message": "UnifiWallet Payment Gateway\nVerify settlement destination\norg: org_7Kd2mQx9\ndestination: dst_9k2Qm\naddress: 0x8f2a4c7b91e0d3a6",
    "instructions": "Sign this message with the destination wallet and POST it to /verify."
  },
  "created_at": "2026-09-04T09:00:00Z"
}
DELETE /v1/destinations/{id} Remove a destination

Stops a destination receiving. Payments already created keep the address they snapshotted — removing it here does not redirect money that is already on its way. Bearer tokens are refused.

Signed request. Scope account:write. Bearer tokens are refused.

URL

https://api.payment.unifiwallet.com/v1/destinations/dst_9k2Qm

Parameters

Path

NameTypeRequiredDescription
idstringYesThe destination id, dst_…

Errors

StatusCodeWhen
403signature_requiredA bearer token was used. Sign the request.
404destination_not_foundNo destination with that id on this account.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const removed = await call('DELETE', '/v1/destinations/dst_9k2Qm');

Response 200

{
  "id": "dst_9k2Qm",
  "object": "settlement_destination",
  "kind": "address",
  "resolved_address": "0x8f2a4c7b91e0d3a6",
  "chains": [
    8453,
    1
  ],
  "asset": "USDC",
  "status": "disabled",
  "usable_at": "2026-09-05T09:00:00Z",
  "verification": {
    "method": "signature",
    "message": "UnifiWallet Payment Gateway\nVerify settlement destination\norg: org_7Kd2mQx9\ndestination: dst_9k2Qm\naddress: 0x8f2a4c7b91e0d3a6",
    "instructions": "Sign this message with the destination wallet and POST it to /verify."
  },
  "created_at": "2026-09-04T09:00:00Z"
}
GET /v1/destinations List destinations

Every settlement destination on the account.

Signed request. Scope account:read.

URL

https://api.payment.unifiwallet.com/v1/destinations

Parameters

This request has no parameters.

Errors

No route-specific errors beyond the common ones above.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const destinations = await call('GET', '/v1/destinations');

Response 200

{
  "object": "list",
  "data": [
    {
      "id": "dst_9k2Qm",
      "object": "settlement_destination",
      "kind": "address",
      "resolved_address": "0x8f2a4c7b91e0d3a6",
      "chains": [
        8453,
        1
      ],
      "asset": "USDC",
      "status": "pending_verification",
      "usable_at": "2026-09-05T09:00:00Z",
      "verification": {
        "method": "signature",
        "message": "UnifiWallet Payment Gateway\nVerify settlement destination\norg: org_7Kd2mQx9\ndestination: dst_9k2Qm\naddress: 0x8f2a4c7b91e0d3a6",
        "instructions": "Sign this message with the destination wallet and POST it to /verify."
      },
      "created_at": "2026-09-04T09:00:00Z"
    }
  ]
}
GET /v1/destinations/{id} Retrieve a destination

One destination. While proof is outstanding it also returns the verification message, so closing the page is not a dead end.

Signed request. Scope account:read.

URL

https://api.payment.unifiwallet.com/v1/destinations/dst_9k2Qm

Parameters

Path

NameTypeRequiredDescription
idstringYesThe destination id, dst_…

Errors

StatusCodeWhen
404destination_not_foundNo such destination, or it belongs to another account.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const destination = await call('GET', '/v1/destinations/dst_9k2Qm');

Response 200

{
  "id": "dst_9k2Qm",
  "object": "settlement_destination",
  "kind": "address",
  "resolved_address": "0x8f2a4c7b91e0d3a6",
  "chains": [
    8453,
    1
  ],
  "asset": "USDC",
  "status": "pending_verification",
  "usable_at": "2026-09-05T09:00:00Z",
  "verification": {
    "method": "signature",
    "message": "UnifiWallet Payment Gateway\nVerify settlement destination\norg: org_7Kd2mQx9\ndestination: dst_9k2Qm\naddress: 0x8f2a4c7b91e0d3a6",
    "instructions": "Sign this message with the destination wallet and POST it to /verify."
  },
  "created_at": "2026-09-04T09:00:00Z"
}
GET /v1/destinations/{id}/balance On-chain balance

The live balance of your registered address, read from the chain. Labelled as your wallet, not an account with us.

Signed request. Scope account:read.

URL

https://api.payment.unifiwallet.com/v1/destinations/dst_9k2Qm/balance

Parameters

Path

NameTypeRequiredDescription
idstringYesThe destination id, dst_…

Errors

StatusCodeWhen
404destination_not_foundNo such destination, or it belongs to another account.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const balance = await call('GET', '/v1/destinations/dst_9k2Qm/balance');

Response 200

{
  "destination_id": "dst_9k2Qm",
  "address": "0x8f2a4c7b91e0d3a6",
  "asset": "USDC",
  "chain_id": 8453,
  "amount": "21450.20",
  "source": "chain"
}
POST /v1/destinations/{id}/verify Prove ownership

Submit a signature of the verification message, made with the destination wallet. Bearer tokens are refused.

Signed request. Scope account:write. Bearer tokens are refused.

URL

https://api.payment.unifiwallet.com/v1/destinations/dst_9k2Qm/verify

Parameters

Path

NameTypeRequiredDescription
idstringYesThe destination id, dst_…

Body

NameTypeRequiredDescription
signaturestringYesSignature of the verification.message, from the destination wallet.

Errors

StatusCodeWhen
400invalid_signatureThe signature does not match the destination address.
404destination_not_foundNo such destination, or it belongs to another account.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const destination = await call('POST', '/v1/destinations/dst_9k2Qm/verify', {
    "signature": "0xabcdef"
  });

Response 200

{
  "id": "dst_9k2Qm",
  "object": "settlement_destination",
  "kind": "address",
  "resolved_address": "0x8f2a4c7b91e0d3a6",
  "chains": [
    8453,
    1
  ],
  "asset": "USDC",
  "status": "active",
  "usable_at": "2026-09-05T09:00:00Z",
  "created_at": "2026-09-04T09:00:00Z",
  "verified_at": "2026-09-04T09:05:00Z"
}

Refunds

We never received the payment, so we cannot send it back. You record what is owed, send it from your own wallet, and we verify the transfer.

  1. POST /v1/refunds Record a refund
  2. POST /v1/refunds/{id}/prepare Build the fulfilment transaction
  3. POST /v1/refunds/{id}/submit Submit the fulfilment hash
  4. POST /v1/refunds/{id}/cancel Withdraw a refund
  5. GET /v1/refunds List refunds
  6. GET /v1/refunds/{id} Retrieve a refund
POST /v1/refunds Record a refund

Creates an obligation. Nothing moves. Bearer tokens are refused.

Signed request. Scope refunds:write. Idempotency key required. Bearer tokens are refused.

URL

https://api.payment.unifiwallet.com/v1/refunds

Parameters

Headers

NameTypeRequiredDescription
Idempotency-KeystringYesRequired. Reuse it on retry.

Body

NameTypeRequiredDescription
payment_idstringYesThe completed payment this refunds.
amountstring—Decimal string. Omit for the full remaining amount.
to_addressstring—Override the destination. Defaults to the buyer’s refund address, then the paying address.
reasonstring—Yours. Maximum 500 characters.

Errors

StatusCodeWhen
400no_refund_destinationNo refund address is known. Ask the buyer to confirm one.
400refund_destination_unusableThe paying address is a contract and cannot receive an arbitrary token.
400refund_exceeds_settlementMore than arrived. The message says what remains.
404payment_not_foundNo payment with that id.
409not_refundableThe payment is not in a state that can be refunded.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const refund = await call('POST', '/v1/refunds', {
    "payment_id": "pay_3nQ8xK2mLp4R",
    "amount": "20.00",
    "reason": "Customer returned one ticket"
  });

Response 201

{
  "id": "ref_7mP2wL8cYt5N",
  "object": "refund",
  "payment_id": "pay_3nQ8xK2mLp4R",
  "kind": "merchant_funded",
  "amount": "20.00",
  "asset": "USDC",
  "chain_id": 8453,
  "to_address": "0x4b2c91e0d3a68f2a",
  "destination_kind": "buyer_refund_address",
  "state": "requested",
  "sla_due_at": "2026-09-07T11:00:00Z",
  "reason": "Customer returned one ticket",
  "created_at": "2026-09-04T12:00:00Z"
}
POST /v1/refunds/{id}/prepare Build the fulfilment transaction

Returns the exact unsigned transaction to send from your wallet. We are not in this step. Bearer tokens are refused.

Signed request. Scope refunds:write. Bearer tokens are refused.

URL

https://api.payment.unifiwallet.com/v1/refunds/ref_7mP2wL8cYt5N/prepare

Parameters

Path

NameTypeRequiredDescription
idstringYesThe refund id, ref_…

Errors

StatusCodeWhen
404refund_not_foundNo such refund, or it belongs to another account.
409not_preparableThe refund is already fulfilled or cancelled.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const prepared = await call('POST', '/v1/refunds/ref_7mP2wL8cYt5N/prepare');

Response 200

{
  "refund_id": "ref_7mP2wL8cYt5N",
  "chain_id": 8453,
  "transaction": {
    "to": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    "data": "0xa9059cbb…",
    "value": "0x0"
  }
}
POST /v1/refunds/{id}/submit Submit the fulfilment hash

Tell us the transaction you sent. We verify it on chain before marking the refund done. Bearer tokens are refused.

Signed request. Scope refunds:write. Idempotency key required. Bearer tokens are refused.

URL

https://api.payment.unifiwallet.com/v1/refunds/ref_7mP2wL8cYt5N/submit

Parameters

Path

NameTypeRequiredDescription
idstringYesThe refund id, ref_…

Body

NameTypeRequiredDescription
tx_hashstringYesThe hash of the transfer you sent.

Errors

StatusCodeWhen
400transfer_already_usedThat transfer settled another refund. Send a separate one.
404refund_not_foundNo such refund, or it belongs to another account.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const refund = await call('POST', '/v1/refunds/ref_7mP2wL8cYt5N/submit', {
    "tx_hash": "0xabc123"
  });

Response 200

{
  "id": "ref_7mP2wL8cYt5N",
  "object": "refund",
  "payment_id": "pay_3nQ8xK2mLp4R",
  "kind": "merchant_funded",
  "amount": "20.00",
  "asset": "USDC",
  "chain_id": 8453,
  "to_address": "0x4b2c91e0d3a68f2a",
  "destination_kind": "buyer_refund_address",
  "state": "fulfilment_pending",
  "sla_due_at": "2026-09-07T11:00:00Z",
  "reason": "Customer returned one ticket",
  "created_at": "2026-09-04T12:00:00Z",
  "fulfilment_tx_hash": "0xabc123"
}
POST /v1/refunds/{id}/cancel Withdraw a refund

Cancels an obligation that has not been paid yet.

Signed request. Scope refunds:write.

URL

https://api.payment.unifiwallet.com/v1/refunds/ref_7mP2wL8cYt5N/cancel

Parameters

Path

NameTypeRequiredDescription
idstringYesThe refund id, ref_…

Errors

StatusCodeWhen
409not_cancellableThe refund has already been submitted or fulfilled.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const refund = await call('POST', '/v1/refunds/ref_7mP2wL8cYt5N/cancel');

Response 200

{
  "id": "ref_7mP2wL8cYt5N",
  "object": "refund",
  "payment_id": "pay_3nQ8xK2mLp4R",
  "kind": "merchant_funded",
  "amount": "20.00",
  "asset": "USDC",
  "chain_id": 8453,
  "to_address": "0x4b2c91e0d3a68f2a",
  "destination_kind": "buyer_refund_address",
  "state": "cancelled",
  "sla_due_at": "2026-09-07T11:00:00Z",
  "reason": "Customer returned one ticket",
  "created_at": "2026-09-04T12:00:00Z"
}
GET /v1/refunds List refunds

Newest first, including deadlines (sla_due_at).

Signed request. Scope payments:read.

URL

https://api.payment.unifiwallet.com/v1/refunds?limit=50

Parameters

Query

NameTypeRequiredDescription
limitinteger—Page size. Default 50, maximum 100.
starting_afterstring—Cursor from the previous page — the list’s next_cursor.

Errors

No route-specific errors beyond the common ones above.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const refunds = await call('GET', '/v1/refunds?limit=50');

Response 200

{
  "object": "list",
  "data": [
    {
      "id": "ref_7mP2wL8cYt5N",
      "object": "refund",
      "payment_id": "pay_3nQ8xK2mLp4R",
      "kind": "merchant_funded",
      "amount": "20.00",
      "asset": "USDC",
      "chain_id": 8453,
      "to_address": "0x4b2c91e0d3a68f2a",
      "destination_kind": "buyer_refund_address",
      "state": "requested",
      "sla_due_at": "2026-09-07T11:00:00Z",
      "reason": "Customer returned one ticket",
      "created_at": "2026-09-04T12:00:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
GET /v1/refunds/{id} Retrieve a refund

One refund, including fulfilment state and the verified transaction if it has one.

Signed request. Scope payments:read.

URL

https://api.payment.unifiwallet.com/v1/refunds/ref_7mP2wL8cYt5N

Parameters

Path

NameTypeRequiredDescription
idstringYesThe refund id, ref_…

Errors

StatusCodeWhen
404refund_not_foundNo such refund, or it belongs to another account.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const refund = await call('GET', '/v1/refunds/ref_7mP2wL8cYt5N');

Response 200

{
  "id": "ref_7mP2wL8cYt5N",
  "object": "refund",
  "payment_id": "pay_3nQ8xK2mLp4R",
  "kind": "merchant_funded",
  "amount": "20.00",
  "asset": "USDC",
  "chain_id": 8453,
  "to_address": "0x4b2c91e0d3a68f2a",
  "destination_kind": "buyer_refund_address",
  "state": "requested",
  "sla_due_at": "2026-09-07T11:00:00Z",
  "reason": "Customer returned one ticket",
  "created_at": "2026-09-04T12:00:00Z"
}

Webhooks

The webhook is the truth. Register a URL, verify the signature on the raw body, and answer 2xx before you do the work.

  1. DELETE /v1/webhook-endpoints/{id} Disable an endpoint
  2. POST /v1/webhook-deliveries/{id}/retry Retry a delivery
  3. GET /v1/events List events
  4. POST /v1/webhook-endpoints Register an endpoint
  5. GET /v1/webhook-endpoints List endpoints
  6. POST /v1/webhook-endpoints/{id}/rotate-secret Rotate the secret
  7. POST /v1/webhook-endpoints/{id}/test Send a test event
  8. GET /v1/webhook-deliveries List deliveries
  9. POST /v1/events/{id}/replay Replay an event
DELETE /v1/webhook-endpoints/{id} Disable an endpoint

Stops delivery to that URL. Events still happen and are still recorded; nothing is sent there. Bearer tokens are refused.

Signed request. Scope webhooks:manage. Bearer tokens are refused.

URL

https://api.payment.unifiwallet.com/v1/webhook-endpoints/whe_5m2Kd9

Parameters

Path

NameTypeRequiredDescription
idstringYesThe endpoint id, whe_…

Errors

StatusCodeWhen
403signature_requiredA bearer token was used. Sign the request.
404endpoint_not_foundNo endpoint with that id on this account.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const removed = await call('DELETE', '/v1/webhook-endpoints/whe_5m2Kd9');

Response 200

{
  "id": "we_5cT1pH9sLu3W",
  "object": "webhook_endpoint",
  "url": "https://shop.example.com/webhooks/unifiwallet",
  "events": [
    "payment.completed",
    "payment.expired",
    "refund.completed"
  ],
  "api_version": "2026-08-28",
  "status": "disabled",
  "mode": "test",
  "consecutive_failures": 0,
  "created_at": "2026-09-04T09:10:00Z"
}
POST /v1/webhook-deliveries/{id}/retry Retry a delivery

Queues one delivery to be sent again, now. For a delivery that exhausted its retries while your endpoint was down.

Signed request. Scope webhooks:manage.

URL

https://api.payment.unifiwallet.com/v1/webhook-deliveries/whd_3k9Qm2/retry

Parameters

Path

NameTypeRequiredDescription
idstringYesThe delivery id, whd_…

Errors

StatusCodeWhen
404delivery_not_foundNo delivery with that id on this account, in this mode.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const retried = await call('POST', '/v1/webhook-deliveries/whd_3k9Qm2/retry');

Response 200

{
  "id": "whd_3k9Qm2",
  "state": "pending"
}
GET /v1/events List events

Every event we recorded, whether or not an endpoint received it. This is the polling fallback: if your endpoint was down, read from here rather than asking us to replay blindly.

Signed request. Scope webhooks:manage.

URL

https://api.payment.unifiwallet.com/v1/events?type=payment.completed

Parameters

Query

NameTypeRequiredDescription
typestring—Filter to one event type, such as payment.completed.

Errors

No route-specific errors beyond the common ones above.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const events = await call('GET', '/v1/events?type=payment.completed');

Response 200

{
  "object": "list",
  "data": [
    {
      "id": "evt_9f3aKd2mQx7b",
      "type": "payment.completed",
      "mode": "test",
      "created_at": "2026-09-04T11:14:22Z"
    }
  ]
}
POST /v1/webhook-endpoints Register an endpoint

HTTPS only, and the host must be a public internet address. The secret is returned once and never again. Bearer tokens are refused.

Signed request. Scope webhooks:manage. Bearer tokens are refused.

URL

https://api.payment.unifiwallet.com/v1/webhook-endpoints

Parameters

Body

NameTypeRequiredDescription
urlstringYesHTTPS URL we will POST events to. Private, loopback and metadata addresses are refused.
eventsarray—Event types to send. Empty means all current types. The live list is GET /v1/capabilities.

Errors

StatusCodeWhen
400unsafe_webhook_urlThe URL is not a public HTTPS host. SSRF targets are refused.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const endpoint = await call('POST', '/v1/webhook-endpoints', {
    "url": "https://shop.example.com/webhooks/unifiwallet",
    "events": [
      "payment.completed",
      "payment.expired",
      "refund.completed"
    ]
  });

Response 201

{
  "id": "we_5cT1pH9sLu3W",
  "object": "webhook_endpoint",
  "url": "https://shop.example.com/webhooks/unifiwallet",
  "events": [
    "payment.completed",
    "payment.expired",
    "refund.completed"
  ],
  "api_version": "2026-08-28",
  "status": "active",
  "mode": "test",
  "consecutive_failures": 0,
  "created_at": "2026-09-04T09:10:00Z",
  "secret": "whsec_9f3aKd2mQx7b"
}
GET /v1/webhook-endpoints List endpoints

Registered URLs. Secrets are never returned on a read.

Signed request. Scope webhooks:manage.

URL

https://api.payment.unifiwallet.com/v1/webhook-endpoints

Parameters

This request has no parameters.

Errors

No route-specific errors beyond the common ones above.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const endpoints = await call('GET', '/v1/webhook-endpoints');

Response 200

{
  "object": "list",
  "data": [
    {
      "id": "we_5cT1pH9sLu3W",
      "object": "webhook_endpoint",
      "url": "https://shop.example.com/webhooks/unifiwallet",
      "events": [
        "payment.completed",
        "payment.expired",
        "refund.completed"
      ],
      "api_version": "2026-08-28",
      "status": "active",
      "mode": "test",
      "consecutive_failures": 0,
      "created_at": "2026-09-04T09:10:00Z"
    }
  ]
}
POST /v1/webhook-endpoints/{id}/rotate-secret Rotate the secret

Issues a new secret. Both the old and the new verify during the overlap, so you can migrate without dropping events. Bearer tokens are refused.

Signed request. Scope webhooks:manage. Bearer tokens are refused.

URL

https://api.payment.unifiwallet.com/v1/webhook-endpoints/we_5cT1pH9sLu3W/rotate-secret

Parameters

Path

NameTypeRequiredDescription
idstringYesThe endpoint id, we_…

Errors

StatusCodeWhen
404endpoint_not_foundNo such endpoint, or it belongs to another account.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const endpoint = await call('POST', '/v1/webhook-endpoints/we_5cT1pH9sLu3W/rotate-secret');

Response 200

{
  "id": "we_5cT1pH9sLu3W",
  "object": "webhook_endpoint",
  "url": "https://shop.example.com/webhooks/unifiwallet",
  "events": [
    "payment.completed",
    "payment.expired",
    "refund.completed"
  ],
  "api_version": "2026-08-28",
  "status": "active",
  "mode": "test",
  "consecutive_failures": 0,
  "created_at": "2026-09-04T09:10:00Z",
  "secret": "whsec_rotated",
  "notice": "Both signatures are sent while you migrate. Verify against either v1 value."
}
POST /v1/webhook-endpoints/{id}/test Send a test event

Pushes a synthetic event at the endpoint so you can confirm verification and your handler before real payments arrive.

Signed request. Scope webhooks:manage.

URL

https://api.payment.unifiwallet.com/v1/webhook-endpoints/we_5cT1pH9sLu3W/test

Parameters

Path

NameTypeRequiredDescription
idstringYesThe endpoint id, we_…

Errors

StatusCodeWhen
404endpoint_not_foundNo such endpoint, or it belongs to another account.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const delivery = await call('POST', '/v1/webhook-endpoints/we_5cT1pH9sLu3W/test');

Response 200

{
  "id": "del_1",
  "object": "webhook_delivery",
  "event_type": "endpoint.test",
  "status_code": 200,
  "success": true
}
GET /v1/webhook-deliveries List deliveries

Attempts, responses and errors. Use this when an event looks missing.

Signed request. Scope webhooks:manage.

URL

https://api.payment.unifiwallet.com/v1/webhook-deliveries?limit=50

Parameters

Query

NameTypeRequiredDescription
limitinteger—Page size. Default 50, maximum 100.
starting_afterstring—Cursor from the previous page — the list’s next_cursor.

Errors

No route-specific errors beyond the common ones above.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const deliveries = await call('GET', '/v1/webhook-deliveries?limit=50');

Response 200

{
  "object": "list",
  "data": [
    {
      "id": "del_8k",
      "object": "webhook_delivery",
      "event_id": "evt_9f3aKd2mQx7b",
      "event_type": "payment.completed",
      "status_code": 200,
      "success": true,
      "created_at": "2026-09-04T11:14:23Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
POST /v1/events/{id}/replay Replay an event

Re-sends an event you missed. Delivery is at-least-once, so your handler must be idempotent on event.id.

Signed request. Scope webhooks:manage.

URL

https://api.payment.unifiwallet.com/v1/events/evt_9f3aKd2mQx7b/replay

Parameters

Path

NameTypeRequiredDescription
idstringYesThe event id, evt_…

Errors

StatusCodeWhen
404event_not_foundNo such event, or it belongs to another account.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const event = await call('POST', '/v1/events/evt_9f3aKd2mQx7b/replay');

Response 200

{
  "id": "evt_9f3aKd2mQx7b",
  "type": "payment.completed",
  "replayed": true
}

Account and reference

What the account can do, which assets and chains we support, and the public liveness check.

  1. PATCH /v1/account/preferences Change how you take money
  2. PATCH /v1/account/settlement Change settlement preferences
  3. PUT /v1/account/accepted-assets Change what you accept
  4. POST /v1/account/domains Claim a domain
  5. POST /v1/account/domains/{domain}/verify Check the DNS challenge
  6. DELETE /v1/account/domains/{domain} Stop trusting a domain
  7. GET /v1/capabilities Scopes and event types
  8. GET /v1/assets Supported assets
  9. GET /v1/chains Supported chains
  10. GET /v1/rates A rate between two assets
  11. GET /v1/fees Your fee plan
  12. GET /v1/account The account
  13. GET /v1/audit-log Audit log
  14. GET /v1/status Liveness
  15. GET /v1/status/detail Operational detail
PATCH /v1/account/preferences Change how you take money

Whether payments are held before they are charged, how long a hold lasts, and who carries the network fee. None of these can move money anywhere new, which is why this is an ordinary write — unlike changing where you settle.

Signed request. Scope account:write.

URL

https://api.payment.unifiwallet.com/v1/account/preferences

Parameters

Body

NameTypeRequiredDescription
capture_methodstring—automatic or manual. Manual holds each payment until you capture it.
hold_window_hoursinteger—How long a held payment waits before the buyer may reclaim it. Capped by the platform maximum.
gas_payerstring—buyer or merchant. Who carries the network fee.
rate_modestring—fixed_rate or floating_rate, for new payments.

Errors

StatusCodeWhen
400manual_capture_unavailableHolds are not switched on for this deployment.
400window_above_ceilingLonger than the platform allows. The message says the maximum.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const account = await call('PATCH', '/v1/account/preferences', {
    "capture_method": "manual",
    "hold_window_hours": 168,
    "gas_payer": "merchant"
  });

Response 200

{
  "capture_method": "manual",
  "gas_payer": "merchant",
  "hold_window_hours": 168,
  "rate_mode": "fixed_rate"
}
PATCH /v1/account/settlement Change settlement preferences

Which verified destination, asset and chain new payments settle to. A key may point settlement at a destination somebody already proved they own; it may not create one, and it may not turn on per-payment routing. Bearer tokens are refused.

Signed request. Scope account:write. Bearer tokens are refused.

URL

https://api.payment.unifiwallet.com/v1/account/settlement

Parameters

Body

NameTypeRequiredDescription
destination_idstring—A verified, active destination on this account.
assetstring—Settlement asset symbol.
chain_idinteger—Settlement chain.
modestring—convert_all, keep_native, split or per_asset.
max_slippage_bpsinteger—Conversion slippage bound, in basis points.

Errors

StatusCodeWhen
400unknown_destinationNo destination with that id on this account.
400destination_not_verifiedVerify the destination before making it the default.
403signature_requiredA bearer token was used. Sign the request.
403panel_onlyPer-payment routing is switched on from the panel, by a person, with their password.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const account = await call('PATCH', '/v1/account/settlement', {
    "destination_id": "dst_9k2Qm",
    "asset": "USDC",
    "chain_id": 8453
  });

Response 200

{
  "object": "account",
  "settlement": {
    "destination_id": "dst_9k2Qm",
    "asset": "USDC",
    "chain_id": 8453,
    "mode": "convert_all"
  }
}
PUT /v1/account/accepted-assets Change what you accept

Which chains and assets a buyer may pay in. Narrowing this does not affect payments already created — they keep the terms they were created with.

Signed request. Scope account:write.

URL

https://api.payment.unifiwallet.com/v1/account/accepted-assets

Parameters

Body

NameTypeRequiredDescription
modestring—all_supported or allowlist.
chains_allowedarray—Chain ids, when the mode is allowlist.
assets_deniedarray—Asset symbols to refuse.
min_payment_usdstring—Below this a payment costs more to collect than it is worth.
underpayment_tolerance_bpsinteger—How far the rate may drift between quote and transaction before a fixed-rate payment is repriced. Not a shortfall allowance — a deposit address enforces the exact amount on chain.

Errors

No route-specific errors beyond the common ones above.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const account = await call('PUT', '/v1/account/accepted-assets', {
    "mode": "allowlist",
    "chains_allowed": [
      8453
    ],
    "assets_denied": [
      "DAI"
    ]
  });

Response 200

{
  "object": "account",
  "accepted": {
    "mode": "allowlist",
    "chains_allowed": [
      8453
    ]
  }
}
POST /v1/account/domains Claim a domain

Claims a domain so your return, cancel and failure URLs may point at it. Returns a DNS TXT challenge; the claim proves nothing until the record is published and checked. Bearer tokens are refused.

Signed request. Scope account:write. Bearer tokens are refused.

URL

https://api.payment.unifiwallet.com/v1/account/domains

Parameters

Body

NameTypeRequiredDescription
domainstringYesThe hostname, without a scheme.

Errors

StatusCodeWhen
403signature_requiredA bearer token was used. Sign the request.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const claim = await call('POST', '/v1/account/domains', {
    "domain": "shop.example.com"
  });

Response 200

{
  "domain": "shop.example.com",
  "record": {
    "name": "_unifiwallet-challenge.shop.example.com",
    "type": "TXT",
    "value": "uw-verify=8f2a4c7b91e0"
  }
}
POST /v1/account/domains/{domain}/verify Check the DNS challenge

Reads the TXT record and, if it matches, trusts the domain. Call it after publishing the record; DNS propagation is why this is a separate step rather than part of the claim.

Signed request. Scope account:write. Bearer tokens are refused.

URL

https://api.payment.unifiwallet.com/v1/account/domains/shop.example.com/verify

Parameters

Path

NameTypeRequiredDescription
domainstringYesThe hostname you claimed.

Errors

StatusCodeWhen
400domain_not_verifiedThe TXT record is missing or does not match. DNS may not have propagated yet.
400domain_not_claimedClaim the domain first; there is no challenge to check.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const verified = await call('POST', '/v1/account/domains/shop.example.com/verify');

Response 200

{
  "domain": "shop.example.com",
  "status": "verified"
}
DELETE /v1/account/domains/{domain} Stop trusting a domain

Removes a verified domain. New payments may no longer redirect there; payments already created keep the URLs they were created with.

Signed request. Scope account:write. Bearer tokens are refused.

URL

https://api.payment.unifiwallet.com/v1/account/domains/shop.example.com

Parameters

Path

NameTypeRequiredDescription
domainstringYesThe hostname to remove.

Errors

No route-specific errors beyond the common ones above.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const removed = await call('DELETE', '/v1/account/domains/shop.example.com');

Response 200

{
  "domain": "shop.example.com",
  "status": "removed"
}
GET /v1/capabilities Scopes and event types

Every scope a key can be granted and every event type we emit, with descriptions. Read this rather than copying a table into your code. Authenticated, but needs no specific scope.

Signed request.

URL

https://api.payment.unifiwallet.com/v1/capabilities

Parameters

This request has no parameters.

Errors

No route-specific errors beyond the common ones above.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const capabilities = await call('GET', '/v1/capabilities');

Response 200

{
  "object": "capabilities",
  "scopes": [
    {
      "scope": "payments:read",
      "default": true,
      "description": "Read payments, refunds and subscriptions."
    },
    {
      "scope": "payments:write",
      "default": true,
      "description": "Create, cancel and expire payments and subscriptions."
    }
  ],
  "events": [
    "payment.created",
    "payment.completed",
    "payment.expired"
  ]
}
GET /v1/assets Supported assets

Assets we accept, per chain, with decimals. USDT is 6 on Ethereum and 18 on BNB Chain — do not assume.

Signed request. Scope payments:read.

URL

https://api.payment.unifiwallet.com/v1/assets

Parameters

This request has no parameters.

Errors

No route-specific errors beyond the common ones above.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const assets = await call('GET', '/v1/assets');

Response 200

{
  "object": "list",
  "data": [
    {
      "symbol": "USDC",
      "chain_id": 8453,
      "decimals": 6,
      "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
    }
  ]
}
GET /v1/chains Supported chains

Chains, confirmation policy, and whether we are watching them right now.

Signed request. Scope payments:read.

URL

https://api.payment.unifiwallet.com/v1/chains

Parameters

This request has no parameters.

Errors

No route-specific errors beyond the common ones above.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const chains = await call('GET', '/v1/chains');

Response 200

{
  "object": "list",
  "data": [
    {
      "chain_id": 8453,
      "name": "Base",
      "confirmations": 20,
      "available": true
    }
  ]
}
GET /v1/rates A rate between two assets

The rate we would use right now. from and to are required.

Signed request. Scope payments:read.

URL

https://api.payment.unifiwallet.com/v1/rates?from=USD&to=USDC

Parameters

Query

NameTypeRequiredDescription
fromstringYesSource asset or fiat code.
tostringYesDestination asset or fiat code.

Errors

StatusCodeWhen
400missing_paramsfrom and to are required.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const rate = await call('GET', '/v1/rates?from=USD&to=USDC');

Response 200

{
  "from": "USD",
  "to": "USDC",
  "rate": "1.0000",
  "source": "oracle",
  "as_of": "2026-09-04T11:00:00Z"
}
GET /v1/fees Your fee plan

The plan snapshotted onto new payments. Changing it does not reprice payments that already exist.

Signed request. Scope payments:read.

URL

https://api.payment.unifiwallet.com/v1/fees

Parameters

This request has no parameters.

Errors

No route-specific errors beyond the common ones above.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const fees = await call('GET', '/v1/fees');

Response 200

{
  "object": "fee_plan",
  "version": "plan_std_v3",
  "gateway_fee_bps": 100,
  "routing_fee_bps": 0,
  "floating_rate_discount_bps": 10,
  "minimum_fee_usd": "0.30",
  "tier": 1
}
GET /v1/account The account

Accepted assets, settlement, verified domains, tier. Credentials are not on this plane — a key cannot mint a key.

Signed request. Scope account:read.

URL

https://api.payment.unifiwallet.com/v1/account

Parameters

This request has no parameters.

Errors

No route-specific errors beyond the common ones above.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const account = await call('GET', '/v1/account');

Response 200

{
  "id": "org_7Kd2mQx9",
  "object": "account",
  "name": "Example Shop",
  "settlement": {
    "asset": "USDC",
    "chain_id": 8453,
    "mode": "convert_all"
  },
  "capture_method": "automatic",
  "gas_payer": "buyer",
  "hold_window_hours": 168,
  "hold_window_max_hours": 720,
  "rate_mode": "fixed_rate",
  "verified_domains": [
    "shop.example.com"
  ],
  "tier": 1,
  "status": "active"
}
GET /v1/audit-log Audit log

Everything consequential anyone did on the account — payments created, destinations verified, keys rotated from the panel.

Signed request. Scope account:read.

URL

https://api.payment.unifiwallet.com/v1/audit-log?limit=50

Parameters

Query

NameTypeRequiredDescription
limitinteger—Page size. Default 50, maximum 100.
starting_afterstring—Cursor from the previous page — the list’s next_cursor.

Errors

No route-specific errors beyond the common ones above.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const entries = await call('GET', '/v1/audit-log?limit=50');

Response 200

{
  "object": "list",
  "data": [
    {
      "action": "payment.create",
      "target": "pay_3nQ8xK2mLp4R",
      "actor": "key_live_1",
      "created_at": "2026-09-04T11:00:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
GET /v1/status Liveness

Public. Whether the service is up. Deliberately almost empty — the detailed picture is behind a credential, because watcher lag and relayer balances tell an attacker when to try.

Public — no signature.

URL

https://api.payment.unifiwallet.com/v1/status

Parameters

This request has no parameters.

Errors

No route-specific errors beyond the common ones above.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const status = await call('GET', '/v1/status');

Response 200

{
  "ok": true
}
GET /v1/status/detail Operational detail

Watcher lag, relayer balances, queue depths. For whoever is on call. Not anonymous.

Signed request. Scope ops:read.

URL

https://api.payment.unifiwallet.com/v1/status/detail

Parameters

This request has no parameters.

Errors

No route-specific errors beyond the common ones above.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const detail = await call('GET', '/v1/status/detail');

Response 200

{
  "ok": true,
  "degraded": false,
  "watchers": [
    {
      "chain_id": 8453,
      "lag_blocks": 0
    }
  ]
}

Test mode

Drive a test payment to any outcome with no wallet and no chain. Refused outright for a live credential.

  1. GET /v1/test/outcomes List simulatable outcomes
  2. POST /v1/test/payments/{id}/simulate Simulate an outcome
GET /v1/test/outcomes List simulatable outcomes

Every outcome you can produce. Use this rather than hard-coding the list.

Signed request. Scope payments:read.

URL

https://api.payment.unifiwallet.com/v1/test/outcomes

Parameters

This request has no parameters.

Errors

StatusCodeWhen
403test_mode_onlySimulation is available to test-mode credentials only.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const outcomes = await call('GET', '/v1/test/outcomes');

Response 200

{
  "object": "list",
  "data": [
    "completed",
    "settled",
    "underpaid",
    "overpaid",
    "expired",
    "failed",
    "settlement_failed",
    "reorged",
    "late_settled"
  ]
}
POST /v1/test/payments/{id}/simulate Simulate an outcome

Drives the real state machine and fires real webhooks. Only the on-chain event is fabricated. Test credentials only.

Signed request. Scope payments:write. Idempotency key required.

URL

https://api.payment.unifiwallet.com/v1/test/payments/pay_3nQ8xK2mLp4R/simulate

Parameters

Path

NameTypeRequiredDescription
idstringYesA test-mode payment id, pay_…

Body

NameTypeRequiredDescription
outcomestring—One of the values from GET /v1/test/outcomes. Defaults to completed.

Errors

StatusCodeWhen
403test_mode_onlySimulation is available to test-mode credentials only.
404payment_not_foundNo test payment with that id.

Authentication failures and rate limits are listed above. Branch on code, never on message.

Sample

call is the signed helper from Authentication. Copy it once; every sample below assumes it.

const payment = await call('POST', '/v1/test/payments/pay_3nQ8xK2mLp4R/simulate', {
    "outcome": "underpaid"
  });

Response 200

{
  "id": "pay_3nQ8xK2mLp4R",
  "object": "payment",
  "mode": "test",
  "status": "underpaid",
  "amount": "129.00",
  "currency": "USD",
  "merchant_order_id": "order_2208",
  "description": "Annual licence",
  "metadata": {
    "campaign": "spring"
  },
  "customer": {
    "reference": "cus_88213",
    "country": "DE"
  },
  "snapshot": {
    "fee_plan_version": "plan_std_v3",
    "fee_bps": 100,
    "capture_method": "automatic",
    "rate_mode": "fixed_rate",
    "settlement": {
      "path": "direct",
      "destination_id": "dst_9k2Qm",
      "resolved_address": "0x8f2a4c7b91e0d3a6",
      "asset": "USDC",
      "chain_id": 8453
    },
    "confirmations_required": 20,
    "tax": {
      "behavior": "exclusive",
      "total": "24.51",
      "jurisdiction": "DE"
    },
    "totals": {
      "subtotal": "129.00",
      "tax": "24.51",
      "total": "153.51"
    }
  },
  "expires_at": "2026-09-04T11:30:00Z",
  "created_at": "2026-09-04T11:00:00Z"
}

Rate limits

Per credential and per address, in bands: reads are generous, writes are tighter, and anything that moves money is tightest. Exceeding one returns 429 with a Retry-After. Back off rather than retrying immediately.

Versioning

The path carries the major version. Additive changes — a new field, a new event type, a new endpoint — happen inside v1 without notice, which is why your client should ignore fields it does not recognise. Anything that would break a working integration gets a new version and a migration period.

Webhooks carry X-UW-Api-Version: 2026-08-28 so you know the body shape.

Looking for something not on this page? All endpoints is generated from the running API and leaves nothing out — no samples, but every field described.