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.
No endpoints match. Try payments, refunds,
or a path like /v1/payments.
Conventions
- Base URL
https://api.payment.unifiwallet.com. - Every request is signed, except
GET /v1/status. - Every
POSTthat creates or moves work needs anIdempotency-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 withstarting_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.
| Status | Code | When |
|---|---|---|
401 | missing_signature | One of the signing headers is absent. See Authentication. |
401 | invalid_signature | The HMAC does not match the bytes you sent. Almost always the body. |
403 | insufficient_scope | The key is real but was not granted the scope this route needs. |
429 | rate_limit_exceeded | Read Retry-After and back off. Do not retry immediately. |
Payments
An intent to be paid a specific amount, with the fee, rate and destination frozen at creation.
-
POST
/v1/paymentsCreate a payment -
GET
/v1/paymentsList payments -
GET
/v1/payments/{id}Retrieve a payment -
GET
/v1/payments/{id}/eventsPayment status history -
POST
/v1/payments/{id}/cancelCancel a payment -
POST
/v1/payments/{id}/expireExpire a payment early -
POST
/v1/payments/{id}/captureCapture an authorization -
POST
/v1/payments/{id}/voidVoid 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
| Name | Type | Required | Description |
|---|---|---|---|
Idempotency-Key | string | Yes | Required on every POST. Reuse it on retry; we return the original response. |
Body
| Name | Type | Required | Description |
|---|---|---|---|
amount | string | Yes | Decimal string in the presentment currency. Never a float. |
currency | string | Yes | Fiat code or asset symbol, 2–10 characters. |
merchant_order_id | string | — | Your reference. Unique per account, and how you search later. |
description | string | — | Shown to the buyer on checkout. Maximum 500 characters. |
line_items | array | — | Optional breakdown. Each item needs name and unit_amount; quantity defaults to 1. |
line_items[].name | string | Yes | Line title. |
line_items[].quantity | integer | — | Integer ≥ 1. Defaults to 1. |
line_items[].unit_amount | string | Yes | Decimal string per unit. |
customer.reference | string | — | Your customer id. Returned on the payment and every webhook about it. |
customer.email | string | — | Used only to send a receipt, and encrypted at rest. |
customer.email_receipt | boolean | — | Whether to email a receipt after the payment completes. |
customer.address.country | string | — | ISO 3166-1 alpha-2. Used for tax jurisdiction. |
customer.address.postal_code | string | — | Billing postal code, when you have it. |
customer.tax_id.type | string | — | Tax identifier type, for example vat. |
customer.tax_id.value | string | — | The identifier itself. |
customer.refund_address | string | — | Preferred refund destination. The buyer can also set this at checkout. |
tax_behavior | string | — | inclusive or exclusive. |
tax_rate_percent | number | — | 0–100. Combined with customer country for the snapshot. |
settlement.destination_id | string | — | A destination you have already verified. Snapshotted onto this payment. |
settlement.address | string | — | Per-payment routing to a 0x address. Refused unless you switched this on in the panel. |
settlement.asset | string | — | Override the settlement asset for this payment only. |
settlement.chain_id | integer | — | Override the settlement chain for this payment only. |
capture_method | string | — | automatic (default) or manual. Manual holds the funds in escrow until you capture; it needs an escrow contract on your settlement chain. |
hold_window_hours | integer | — | 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_mode | string | — | fixed_rate or floating_rate. |
rate_lock_seconds | integer | — | 300–1800. How long a fixed rate is held. |
return_url | string | — | HTTPS URL on a domain you have verified. A hint, not proof of payment. |
cancel_url | string | — | HTTPS URL on a verified domain, for a buyer who backs out. |
failure_url | string | — | HTTPS URL on a verified domain, if you want a dedicated failure page. |
expires_in | integer | — | Seconds until the payment expires. 60–86400. |
fee_payer | string | — | merchant (default) or buyer. Grossing up is done for you. |
gas_payer | string | — | 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. |
discounts | array | — | Reductions applied before tax. Each entry takes a coupon or a flat amount. |
payer_address_hint | string | — | The wallet you expect to pay, when you already know it. |
metadata | object | — | Yours. Returned on the payment and every webhook about it. |
Errors
| Status | Code | When |
|---|---|---|
400 | below_minimum_payment | Under your account floor. Network fees would cost more than the sale. |
400 | no_settlement_destination | Register and verify a wallet first. |
400 | settlement_destination_not_verified | Sign the verification message with that wallet. |
400 | destination_in_cooling_period | The destination is still cooling. The response says until when. |
400 | domain_not_verified | return_url, cancel_url or failure_url is on a domain you have not proved you own. |
400 | manual_capture_unavailable | Holds are not switched on for this deployment, or no escrow contract is deployed on your settlement chain. |
403 | live_not_enabled | Add your business details before taking live payments. |
409 | idempotency_key_reuse | Same 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"
}
});
const payment = await call<Payment>('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"
}
});
payment = 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"
}
})
payment, err := Call("POST", "/v1/payments", map[string]any{
"amount": "129.00",
"currency": "USD",
"merchant_order_id": "order_2208",
"description": "Annual licence",
"line_items": []any{
map[string]any{
"name": "Annual licence",
"quantity": 1,
"unit_amount": "129.00",
},
},
"customer": map[string]any{
"reference": "cus_88213",
"email": "buyer@example.com",
"address": map[string]any{
"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": map[string]any{
"campaign": "spring",
},
})
if err != nil {
return err
}
defer payment.Body.Close()
$payment = uw_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",
],
]);
curl -X POST "$BASE/v1/payments" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{
"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
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
| Name | Type | Required | Description |
|---|---|---|---|
status | string | — | Filter to one status: created, settling, completed, expired, cancelled, and the rest of the lifecycle. |
merchant_order_id | string | — | Your reference. Returns the matching payment, if any. |
limit | integer | — | Page size. Default 50, maximum 100. |
starting_after | string | — | 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');
const payments = await call<PaymentList>('GET', '/v1/payments?status=completed&limit=50');
payments = call("GET", "/v1/payments?status=completed&limit=50")
payments, err := Call("GET", "/v1/payments?status=completed&limit=50", nil)
if err != nil {
return err
}
defer payments.Body.Close()
$payments = uw_call('GET', '/v1/payments?status=completed&limit=50');
curl -X GET "$BASE/v1/payments?status=completed&limit=50" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The payment id, pay_… |
Errors
| Status | Code | When |
|---|---|---|
404 | payment_not_found | No 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');
const payment = await call<Payment>('GET', '/v1/payments/pay_3nQ8xK2mLp4R');
payment = call("GET", "/v1/payments/pay_3nQ8xK2mLp4R")
payment, err := Call("GET", "/v1/payments/pay_3nQ8xK2mLp4R", nil)
if err != nil {
return err
}
defer payment.Body.Close()
$payment = uw_call('GET', '/v1/payments/pay_3nQ8xK2mLp4R');
curl -X GET "$BASE/v1/payments/pay_3nQ8xK2mLp4R" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The payment id, pay_… |
Errors
| Status | Code | When |
|---|---|---|
404 | payment_not_found | No 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');
const events = await call<EventList>('GET', '/v1/payments/pay_3nQ8xK2mLp4R/events');
events = call("GET", "/v1/payments/pay_3nQ8xK2mLp4R/events")
events, err := Call("GET", "/v1/payments/pay_3nQ8xK2mLp4R/events", nil)
if err != nil {
return err
}
defer events.Body.Close()
$events = uw_call('GET', '/v1/payments/pay_3nQ8xK2mLp4R/events');
curl -X GET "$BASE/v1/payments/pay_3nQ8xK2mLp4R/events" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The payment id, pay_… |
Errors
| Status | Code | When |
|---|---|---|
404 | payment_not_found | No such payment, or it belongs to another account. |
409 | not_cancellable | The 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');
const payment = await call<Payment>('POST', '/v1/payments/pay_3nQ8xK2mLp4R/cancel');
payment = call("POST", "/v1/payments/pay_3nQ8xK2mLp4R/cancel")
payment, err := Call("POST", "/v1/payments/pay_3nQ8xK2mLp4R/cancel", nil)
if err != nil {
return err
}
defer payment.Body.Close()
$payment = uw_call('POST', '/v1/payments/pay_3nQ8xK2mLp4R/cancel');
curl -X POST "$BASE/v1/payments/pay_3nQ8xK2mLp4R/cancel" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The payment id, pay_… |
Errors
| Status | Code | When |
|---|---|---|
404 | payment_not_found | No such payment, or it belongs to another account. |
409 | not_expirable | The 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');
const payment = await call<Payment>('POST', '/v1/payments/pay_3nQ8xK2mLp4R/expire');
payment = call("POST", "/v1/payments/pay_3nQ8xK2mLp4R/expire")
payment, err := Call("POST", "/v1/payments/pay_3nQ8xK2mLp4R/expire", nil)
if err != nil {
return err
}
defer payment.Body.Close()
$payment = uw_call('POST', '/v1/payments/pay_3nQ8xK2mLp4R/expire');
curl -X POST "$BASE/v1/payments/pay_3nQ8xK2mLp4R/expire" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The payment id, pay_… |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
Idempotency-Key | string | Yes | Required. Reuse it on retry. |
Body
| Name | Type | Required | Description |
|---|---|---|---|
amount | string | — | Decimal string to capture. Omit for the full authorized amount. |
Errors
| Status | Code | When |
|---|---|---|
400 | escrow_unavailable | Holds are not switched on for this deployment. |
400 | invalid_capture_amount | More than is still held. Check amount_capturable. |
400 | capture_failed | The transaction could not be submitted. Nothing moved; try again. |
403 | signature_required | A bearer token was used. Sign the request. |
409 | not_authorized | Only a payment holding funds can be captured. |
409 | authorization_expired | The hold lapsed and the buyer may reclaim the funds. Ask them to pay again. |
409 | escrow_operation_in_flight | A capture or void is still confirming. Wait — a second one could charge the buyer twice. |
409 | missing_authorization | No 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"
});
const payment = await call<Payment>('POST', '/v1/payments/pay_3nQ8xK2mLp4R/capture', {
"amount": "80.00"
});
payment = call("POST", "/v1/payments/pay_3nQ8xK2mLp4R/capture", {
"amount": "80.00"
})
payment, err := Call("POST", "/v1/payments/pay_3nQ8xK2mLp4R/capture", map[string]any{
"amount": "80.00",
})
if err != nil {
return err
}
defer payment.Body.Close()
$payment = uw_call('POST', '/v1/payments/pay_3nQ8xK2mLp4R/capture', [
"amount" => "80.00",
]);
curl -X POST "$BASE/v1/payments/pay_3nQ8xK2mLp4R/capture" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{
"amount": "80.00"
}'
Response 202
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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The payment id, pay_… |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
Idempotency-Key | string | Yes | Required. Reuse it on retry. |
Errors
| Status | Code | When |
|---|---|---|
400 | escrow_unavailable | Holds are not switched on for this deployment. |
400 | void_failed | The transaction could not be submitted. Nothing moved; try again. |
409 | not_authorized | Only a payment holding funds can be released. |
409 | escrow_operation_in_flight | A capture or void is still confirming. |
409 | missing_authorization | No 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');
const payment = await call<Payment>('POST', '/v1/payments/pay_3nQ8xK2mLp4R/void');
payment = call("POST", "/v1/payments/pay_3nQ8xK2mLp4R/void")
payment, err := Call("POST", "/v1/payments/pay_3nQ8xK2mLp4R/void", nil)
if err != nil {
return err
}
defer payment.Body.Close()
$payment = uw_call('POST', '/v1/payments/pay_3nQ8xK2mLp4R/void');
curl -X POST "$BASE/v1/payments/pay_3nQ8xK2mLp4R/void" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY"
Response 202
Subscriptions
A recurring charge with no stored card. Creating one charges nothing; nothing is billable until the buyer acts.
-
POST
/v1/subscriptionsCreate a subscription -
GET
/v1/subscriptions/{id}/authorizationRead the authorization payload -
POST
/v1/subscriptions/{id}/authorizeSubmit the buyer’s signature -
GET
/v1/subscriptionsList subscriptions -
GET
/v1/subscriptions/{id}Retrieve a subscription -
GET
/v1/subscriptions/{id}/cyclesList cycles -
POST
/v1/subscriptions/{id}/chargeCharge now -
POST
/v1/subscriptions/{id}/cancelCancel 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
| Name | Type | Required | Description |
|---|---|---|---|
Idempotency-Key | string | Yes | Required. Reuse it on retry. |
Body
| Name | Type | Required | Description |
|---|---|---|---|
amount | string | Yes | Decimal string per cycle. Never a float. |
currency | string | Yes | ISO 4217, three letters. |
interval | string | Yes | day, week, month or year. |
interval_count | integer | — | 1–52. How many intervals per cycle. Default 1. |
collection_method | string | — | auto_charge (default) or payment_link. |
description | string | — | Shown to the buyer. Maximum 500 characters. |
merchant_reference | string | — | Your reference for this subscription. |
trial_end | string | — | ISO 8601. No charge before this instant. |
ends_at | string | — | ISO 8601. Hard stop after this instant. |
allowance_multiplier | number | — | 1–12. Headroom above one cycle so a price rise does not need a new signature. |
customer.reference | string | — | Your customer id. |
customer.email | string | — | Used for payment-link invoices and receipts. |
customer.wallet_address | string | — | 0x address that will sign the spend permission. |
customer.address.country | string | — | ISO 3166-1 alpha-2. |
metadata | object | — | Yours. Returned on the subscription and its webhooks. |
settlement.destination_id | string | — | A destination you have already verified. Snapshotted onto this payment. |
settlement.address | string | — | Per-payment routing to a 0x address. Refused unless you switched this on in the panel. |
settlement.asset | string | — | Override the settlement asset for this payment only. |
settlement.chain_id | integer | — | Override the settlement chain for this payment only. |
Errors
| Status | Code | When |
|---|---|---|
400 | below_minimum_payment | The cycle amount is under your account floor. |
400 | auto_charge_unavailable | No spend permission contract on that chain. Use payment_link. |
400 | allowance_below_cycle | The buyer’s cap is under one cycle. Every renewal would decline. |
400 | ends_in_the_past | ends_at is already behind us. |
403 | live_not_enabled | This 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"
}
});
const subscription = await call<Subscription>('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"
}
});
subscription = 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"
}
})
subscription, err := Call("POST", "/v1/subscriptions", map[string]any{
"amount": "19.99",
"currency": "USD",
"interval": "month",
"collection_method": "auto_charge",
"description": "Pro plan",
"customer": map[string]any{
"reference": "cus_88213",
"email": "buyer@example.com",
"wallet_address": "0x4b2c91e0d3a68f2a4c7b",
},
})
if err != nil {
return err
}
defer subscription.Body.Close()
$subscription = uw_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",
],
]);
curl -X POST "$BASE/v1/subscriptions" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{
"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
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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The subscription id, sub_… |
Errors
| Status | Code | When |
|---|---|---|
404 | subscription_not_found | No 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');
const authorization = await call<Authorization>('GET', '/v1/subscriptions/sub_4kR9nM1wQx2P/authorization');
authorization = call("GET", "/v1/subscriptions/sub_4kR9nM1wQx2P/authorization")
authorization, err := Call("GET", "/v1/subscriptions/sub_4kR9nM1wQx2P/authorization", nil)
if err != nil {
return err
}
defer authorization.Body.Close()
$authorization = uw_call('GET', '/v1/subscriptions/sub_4kR9nM1wQx2P/authorization');
curl -X GET "$BASE/v1/subscriptions/sub_4kR9nM1wQx2P/authorization" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The subscription id, sub_… |
Body
| Name | Type | Required | Description |
|---|---|---|---|
permission | object | Yes | The permission object from create or GET …/authorization. Do not invent these fields. |
permission.account | string | Yes | Buyer’s 0x address. |
permission.spender | string | Yes | The spend-permission manager. |
permission.token | string | Yes | ERC-20 the buyer is permitting. |
permission.recipient | string | Yes | Your settlement address. |
permission.fee_recipient | string | Yes | Our fee address, snapshotted at create. |
permission.fee_bps | integer | Yes | Fee in basis points, 0–500. |
permission.allowance | string | Yes | Integer base units, not a decimal. |
permission.period | integer | Yes | Period length in seconds, ≥ 60. |
permission.start | integer | Yes | Unix seconds. |
permission.end | integer | Yes | Unix seconds. |
permission.salt | string | Yes | Integer as a string. Uniqueness for the permission hash. |
permission.chain_id | integer | — | Defaults to the snapshotted chain. |
signature | string | Yes | 0x-prefixed signature from the paying wallet. |
Errors
| Status | Code | When |
|---|---|---|
400 | invalid_signature | The signature does not match the permission. |
409 | subscription_over | It has been cancelled or has ended; there is nothing to authorise. |
400 | recipient_mismatch | The signed permission pays somewhere other than your settlement address. Refused rather than reconciled. |
400 | fee_mismatch | The signed fee is not the one this subscription was created with. |
400 | permission_expired | The 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"
});
const subscription = await call<Subscription>('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"
});
subscription = 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"
})
subscription, err := Call("POST", "/v1/subscriptions/sub_4kR9nM1wQx2P/authorize", map[string]any{
"permission": map[string]any{
"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",
})
if err != nil {
return err
}
defer subscription.Body.Close()
$subscription = uw_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",
]);
curl -X POST "$BASE/v1/subscriptions/sub_4kR9nM1wQx2P/authorize" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{
"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
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
| Name | Type | Required | Description |
|---|---|---|---|
limit | integer | — | Page size. Default 50, maximum 100. |
starting_after | string | — | 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');
const subscriptions = await call<SubscriptionList>('GET', '/v1/subscriptions?limit=50');
subscriptions = call("GET", "/v1/subscriptions?limit=50")
subscriptions, err := Call("GET", "/v1/subscriptions?limit=50", nil)
if err != nil {
return err
}
defer subscriptions.Body.Close()
$subscriptions = uw_call('GET', '/v1/subscriptions?limit=50');
curl -X GET "$BASE/v1/subscriptions?limit=50" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The subscription id, sub_… |
Errors
| Status | Code | When |
|---|---|---|
404 | subscription_not_found | No 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');
const subscription = await call<Subscription>('GET', '/v1/subscriptions/sub_4kR9nM1wQx2P');
subscription = call("GET", "/v1/subscriptions/sub_4kR9nM1wQx2P")
subscription, err := Call("GET", "/v1/subscriptions/sub_4kR9nM1wQx2P", nil)
if err != nil {
return err
}
defer subscription.Body.Close()
$subscription = uw_call('GET', '/v1/subscriptions/sub_4kR9nM1wQx2P');
curl -X GET "$BASE/v1/subscriptions/sub_4kR9nM1wQx2P" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The subscription id, sub_… |
Errors
| Status | Code | When |
|---|---|---|
404 | subscription_not_found | No 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');
const cycles = await call<CycleList>('GET', '/v1/subscriptions/sub_4kR9nM1wQx2P/cycles');
cycles = call("GET", "/v1/subscriptions/sub_4kR9nM1wQx2P/cycles")
cycles, err := Call("GET", "/v1/subscriptions/sub_4kR9nM1wQx2P/cycles", nil)
if err != nil {
return err
}
defer cycles.Body.Close()
$cycles = uw_call('GET', '/v1/subscriptions/sub_4kR9nM1wQx2P/cycles');
curl -X GET "$BASE/v1/subscriptions/sub_4kR9nM1wQx2P/cycles" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The subscription id, sub_… |
Headers
| Name | Type | Required | Description |
|---|---|---|---|
Idempotency-Key | string | Yes | Required. Reuse it on retry. |
Errors
| Status | Code | When |
|---|---|---|
409 | cycle_in_flight | Already being charged. Do not retry, or the buyer pays twice. |
409 | not_chargeable | The subscription is incomplete, cancelled, or ended. |
400 | not_auto_charge | This subscription collects by payment link; there is nothing to pull against. |
409 | cycle_in_flight | That 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');
const subscription = await call<Subscription>('POST', '/v1/subscriptions/sub_4kR9nM1wQx2P/charge');
subscription = call("POST", "/v1/subscriptions/sub_4kR9nM1wQx2P/charge")
subscription, err := Call("POST", "/v1/subscriptions/sub_4kR9nM1wQx2P/charge", nil)
if err != nil {
return err
}
defer subscription.Body.Close()
$subscription = uw_call('POST', '/v1/subscriptions/sub_4kR9nM1wQx2P/charge');
curl -X POST "$BASE/v1/subscriptions/sub_4kR9nM1wQx2P/charge" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The subscription id, sub_… |
Body
| Name | Type | Required | Description |
|---|---|---|---|
at_period_end | boolean | — | If true, finish the current period; if false or omitted, stop now. |
reason | string | — | Yours. Maximum 500 characters. |
Errors
| Status | Code | When |
|---|---|---|
404 | subscription_not_found | No such subscription, or it belongs to another account. |
404 | subscription_not_found | No 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"
});
const subscription = await call<Subscription>('POST', '/v1/subscriptions/sub_4kR9nM1wQx2P/cancel', {
"at_period_end": false,
"reason": "Closed the account"
});
subscription = call("POST", "/v1/subscriptions/sub_4kR9nM1wQx2P/cancel", {
"at_period_end": False,
"reason": "Closed the account"
})
subscription, err := Call("POST", "/v1/subscriptions/sub_4kR9nM1wQx2P/cancel", map[string]any{
"at_period_end": false,
"reason": "Closed the account",
})
if err != nil {
return err
}
defer subscription.Body.Close()
$subscription = uw_call('POST', '/v1/subscriptions/sub_4kR9nM1wQx2P/cancel', [
"at_period_end" => false,
"reason" => "Closed the account",
]);
curl -X POST "$BASE/v1/subscriptions/sub_4kR9nM1wQx2P/cancel" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{
"at_period_end": false,
"reason": "Closed the account"
}'
Response 200
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.
-
GET
/v1/manage/{token}What the buyer sees -
POST
/v1/manage/{token}/cancelThe buyer cancels -
GET
/v1/manage/{token}/revocationThe 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
| Name | Type | Required | Description |
|---|---|---|---|
token | string | Yes | The manage token, ms_… From manage_url when the subscription was created. |
Errors
| Status | Code | When |
|---|---|---|
404 | subscription_not_found | The 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');
const result = await call('GET', '/v1/manage/ms_7cQ2vB9xLp4R');
result = call("GET", "/v1/manage/ms_7cQ2vB9xLp4R")
result, err := Call("GET", "/v1/manage/ms_7cQ2vB9xLp4R", nil)
if err != nil {
return err
}
defer result.Body.Close()
$result = uw_call('GET', '/v1/manage/ms_7cQ2vB9xLp4R');
curl -X GET "$BASE/v1/manage/ms_7cQ2vB9xLp4R" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
token | string | Yes | The manage token, ms_… |
Body
| Name | Type | Required | Description |
|---|---|---|---|
immediately | boolean | — | End it now rather than at the end of the paid period. Default false. |
reason | string | — | Optional, and never required — a cancellation must not be gated on answering a question. |
Errors
| Status | Code | When |
|---|---|---|
404 | subscription_not_found | The link is not valid. |
409 | subscription_ended | It 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
});
const result = await call('POST', '/v1/manage/ms_7cQ2vB9xLp4R/cancel', {
"immediately": false
});
result = call("POST", "/v1/manage/ms_7cQ2vB9xLp4R/cancel", {
"immediately": False
})
result, err := Call("POST", "/v1/manage/ms_7cQ2vB9xLp4R/cancel", map[string]any{
"immediately": false,
})
if err != nil {
return err
}
defer result.Body.Close()
$result = uw_call('POST', '/v1/manage/ms_7cQ2vB9xLp4R/cancel', [
"immediately" => false,
]);
curl -X POST "$BASE/v1/manage/ms_7cQ2vB9xLp4R/cancel" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{
"immediately": false
}'
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
token | string | Yes | The manage token, ms_… |
Errors
| Status | Code | When |
|---|---|---|
404 | subscription_not_found | The link is not valid. |
409 | no_permission | Nothing 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');
const result = await call('GET', '/v1/manage/ms_7cQ2vB9xLp4R/revocation');
result = call("GET", "/v1/manage/ms_7cQ2vB9xLp4R/revocation")
result, err := Call("GET", "/v1/manage/ms_7cQ2vB9xLp4R/revocation", nil)
if err != nil {
return err
}
defer result.Body.Close()
$result = uw_call('GET', '/v1/manage/ms_7cQ2vB9xLp4R/revocation');
curl -X GET "$BASE/v1/manage/ms_7cQ2vB9xLp4R/revocation" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
Settlements
What arrived. There is no balance and no withdraw — these records are the history of money that already landed in your wallet.
-
GET
/v1/settlementsList settlements -
GET
/v1/settlements/{id}Retrieve a settlement -
GET
/v1/settlements/summaryVolume summary -
GET
/v1/statementsFlow 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
| Name | Type | Required | Description |
|---|---|---|---|
payment_id | string | — | Limit to one payment. |
status | string | — | Pass quarantined to see refused events. Default excludes them. |
limit | integer | — | Page size. Default 50, maximum 100. |
starting_after | string | — | 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');
const settlements = await call<SettlementList>('GET', '/v1/settlements?limit=50');
settlements = call("GET", "/v1/settlements?limit=50")
settlements, err := Call("GET", "/v1/settlements?limit=50", nil)
if err != nil {
return err
}
defer settlements.Body.Close()
$settlements = uw_call('GET', '/v1/settlements?limit=50');
curl -X GET "$BASE/v1/settlements?limit=50" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The settlement id, set_… |
Errors
| Status | Code | When |
|---|---|---|
404 | settlement_not_found | No 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');
const settlement = await call<Settlement>('GET', '/v1/settlements/set_2hN6qR4bVk8J');
settlement = call("GET", "/v1/settlements/set_2hN6qR4bVk8J")
settlement, err := Call("GET", "/v1/settlements/set_2hN6qR4bVk8J", nil)
if err != nil {
return err
}
defer settlement.Body.Close()
$settlement = uw_call('GET', '/v1/settlements/set_2hN6qR4bVk8J');
curl -X GET "$BASE/v1/settlements/set_2hN6qR4bVk8J" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
since | string | — | 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');
const summary = await call<SettlementSummary>('GET', '/v1/settlements/summary?since=2026-08-01T00%3A00%3A00Z');
summary = call("GET", "/v1/settlements/summary?since=2026-08-01T00%3A00%3A00Z")
summary, err := Call("GET", "/v1/settlements/summary?since=2026-08-01T00%3A00%3A00Z", nil)
if err != nil {
return err
}
defer summary.Body.Close()
$summary = uw_call('GET', '/v1/settlements/summary?since=2026-08-01T00%3A00%3A00Z');
curl -X GET "$BASE/v1/settlements/summary?since=2026-08-01T00%3A00%3A00Z" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
since | string | — | 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');
const statements = await call<StatementList>('GET', '/v1/statements?since=2026-08-01T00%3A00%3A00Z');
statements = call("GET", "/v1/statements?since=2026-08-01T00%3A00%3A00Z")
statements, err := Call("GET", "/v1/statements?since=2026-08-01T00%3A00%3A00Z", nil)
if err != nil {
return err
}
defer statements.Body.Close()
$statements = uw_call('GET', '/v1/statements?since=2026-08-01T00%3A00%3A00Z');
curl -X GET "$BASE/v1/statements?since=2026-08-01T00%3A00%3A00Z" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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.
-
POST
/v1/destinationsRegister a destination -
DELETE
/v1/destinations/{id}Remove a destination -
GET
/v1/destinationsList destinations -
GET
/v1/destinations/{id}Retrieve a destination -
GET
/v1/destinations/{id}/balanceOn-chain balance -
POST
/v1/destinations/{id}/verifyProve 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
| Name | Type | Required | Description |
|---|---|---|---|
Idempotency-Key | string | Yes | Required. Reuse it on retry. |
Body
| Name | Type | Required | Description |
|---|---|---|---|
kind | string | Yes | address or shield. |
address | string | — | Required when kind is address. |
shield_code | string | — | Required when kind is shield. |
chains | array | — | Chain ids it may receive on. Defaults to every chain the asset exists on. |
asset | string | — | Restrict it to one token. Omit for any accepted token. |
label | string | — | Your name for it, shown in the panel. |
Errors
| Status | Code | When |
|---|---|---|
400 | invalid_address | Not an address, or the zero address. |
400 | exchange_deposit_address | That looks like an exchange deposit address. Arbitrary token arrivals there are commonly lost. |
403 | signature_required | A bearer token was used. Sign the request. |
422 | destination_flagged | The 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"
});
const destination = await call<Destination>('POST', '/v1/destinations', {
"kind": "address",
"address": "0x8f2a4c7b91e0d3a6",
"chains": [
8453
],
"asset": "USDC",
"label": "Treasury"
});
destination = call("POST", "/v1/destinations", {
"kind": "address",
"address": "0x8f2a4c7b91e0d3a6",
"chains": [
8453
],
"asset": "USDC",
"label": "Treasury"
})
destination, err := Call("POST", "/v1/destinations", map[string]any{
"kind": "address",
"address": "0x8f2a4c7b91e0d3a6",
"chains": []any{
8453,
},
"asset": "USDC",
"label": "Treasury",
})
if err != nil {
return err
}
defer destination.Body.Close()
$destination = uw_call('POST', '/v1/destinations', [
"kind" => "address",
"address" => "0x8f2a4c7b91e0d3a6",
"chains" => [8453],
"asset" => "USDC",
"label" => "Treasury",
]);
curl -X POST "$BASE/v1/destinations" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{
"kind": "address",
"address": "0x8f2a4c7b91e0d3a6",
"chains": [
8453
],
"asset": "USDC",
"label": "Treasury"
}'
Response 201
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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The destination id, dst_… |
Errors
| Status | Code | When |
|---|---|---|
403 | signature_required | A bearer token was used. Sign the request. |
404 | destination_not_found | No 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');
const removed = await call<Destination>('DELETE', '/v1/destinations/dst_9k2Qm');
removed = call("DELETE", "/v1/destinations/dst_9k2Qm")
removed, err := Call("DELETE", "/v1/destinations/dst_9k2Qm", nil)
if err != nil {
return err
}
defer removed.Body.Close()
$removed = uw_call('DELETE', '/v1/destinations/dst_9k2Qm');
curl -X DELETE "$BASE/v1/destinations/dst_9k2Qm" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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');
const destinations = await call<DestinationList>('GET', '/v1/destinations');
destinations = call("GET", "/v1/destinations")
destinations, err := Call("GET", "/v1/destinations", nil)
if err != nil {
return err
}
defer destinations.Body.Close()
$destinations = uw_call('GET', '/v1/destinations');
curl -X GET "$BASE/v1/destinations" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The destination id, dst_… |
Errors
| Status | Code | When |
|---|---|---|
404 | destination_not_found | No 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');
const destination = await call<Destination>('GET', '/v1/destinations/dst_9k2Qm');
destination = call("GET", "/v1/destinations/dst_9k2Qm")
destination, err := Call("GET", "/v1/destinations/dst_9k2Qm", nil)
if err != nil {
return err
}
defer destination.Body.Close()
$destination = uw_call('GET', '/v1/destinations/dst_9k2Qm');
curl -X GET "$BASE/v1/destinations/dst_9k2Qm" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The destination id, dst_… |
Errors
| Status | Code | When |
|---|---|---|
404 | destination_not_found | No 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');
const balance = await call<DestinationBalance>('GET', '/v1/destinations/dst_9k2Qm/balance');
balance = call("GET", "/v1/destinations/dst_9k2Qm/balance")
balance, err := Call("GET", "/v1/destinations/dst_9k2Qm/balance", nil)
if err != nil {
return err
}
defer balance.Body.Close()
$balance = uw_call('GET', '/v1/destinations/dst_9k2Qm/balance');
curl -X GET "$BASE/v1/destinations/dst_9k2Qm/balance" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The destination id, dst_… |
Body
| Name | Type | Required | Description |
|---|---|---|---|
signature | string | Yes | Signature of the verification.message, from the destination wallet. |
Errors
| Status | Code | When |
|---|---|---|
400 | invalid_signature | The signature does not match the destination address. |
404 | destination_not_found | No 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"
});
const destination = await call<Destination>('POST', '/v1/destinations/dst_9k2Qm/verify', {
"signature": "0xabcdef"
});
destination = call("POST", "/v1/destinations/dst_9k2Qm/verify", {
"signature": "0xabcdef"
})
destination, err := Call("POST", "/v1/destinations/dst_9k2Qm/verify", map[string]any{
"signature": "0xabcdef",
})
if err != nil {
return err
}
defer destination.Body.Close()
$destination = uw_call('POST', '/v1/destinations/dst_9k2Qm/verify', [
"signature" => "0xabcdef",
]);
curl -X POST "$BASE/v1/destinations/dst_9k2Qm/verify" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{
"signature": "0xabcdef"
}'
Response 200
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.
-
POST
/v1/refundsRecord a refund -
POST
/v1/refunds/{id}/prepareBuild the fulfilment transaction -
POST
/v1/refunds/{id}/submitSubmit the fulfilment hash -
POST
/v1/refunds/{id}/cancelWithdraw a refund -
GET
/v1/refundsList refunds -
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
| Name | Type | Required | Description |
|---|---|---|---|
Idempotency-Key | string | Yes | Required. Reuse it on retry. |
Body
| Name | Type | Required | Description |
|---|---|---|---|
payment_id | string | Yes | The completed payment this refunds. |
amount | string | — | Decimal string. Omit for the full remaining amount. |
to_address | string | — | Override the destination. Defaults to the buyer’s refund address, then the paying address. |
reason | string | — | Yours. Maximum 500 characters. |
Errors
| Status | Code | When |
|---|---|---|
400 | no_refund_destination | No refund address is known. Ask the buyer to confirm one. |
400 | refund_destination_unusable | The paying address is a contract and cannot receive an arbitrary token. |
400 | refund_exceeds_settlement | More than arrived. The message says what remains. |
404 | payment_not_found | No payment with that id. |
409 | not_refundable | The 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"
});
const refund = await call<Refund>('POST', '/v1/refunds', {
"payment_id": "pay_3nQ8xK2mLp4R",
"amount": "20.00",
"reason": "Customer returned one ticket"
});
refund = call("POST", "/v1/refunds", {
"payment_id": "pay_3nQ8xK2mLp4R",
"amount": "20.00",
"reason": "Customer returned one ticket"
})
refund, err := Call("POST", "/v1/refunds", map[string]any{
"payment_id": "pay_3nQ8xK2mLp4R",
"amount": "20.00",
"reason": "Customer returned one ticket",
})
if err != nil {
return err
}
defer refund.Body.Close()
$refund = uw_call('POST', '/v1/refunds', [
"payment_id" => "pay_3nQ8xK2mLp4R",
"amount" => "20.00",
"reason" => "Customer returned one ticket",
]);
curl -X POST "$BASE/v1/refunds" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{
"payment_id": "pay_3nQ8xK2mLp4R",
"amount": "20.00",
"reason": "Customer returned one ticket"
}'
Response 201
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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The refund id, ref_… |
Errors
| Status | Code | When |
|---|---|---|
404 | refund_not_found | No such refund, or it belongs to another account. |
409 | not_preparable | The 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');
const prepared = await call<PreparedRefund>('POST', '/v1/refunds/ref_7mP2wL8cYt5N/prepare');
prepared = call("POST", "/v1/refunds/ref_7mP2wL8cYt5N/prepare")
prepared, err := Call("POST", "/v1/refunds/ref_7mP2wL8cYt5N/prepare", nil)
if err != nil {
return err
}
defer prepared.Body.Close()
$prepared = uw_call('POST', '/v1/refunds/ref_7mP2wL8cYt5N/prepare');
curl -X POST "$BASE/v1/refunds/ref_7mP2wL8cYt5N/prepare" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The refund id, ref_… |
Body
| Name | Type | Required | Description |
|---|---|---|---|
tx_hash | string | Yes | The hash of the transfer you sent. |
Errors
| Status | Code | When |
|---|---|---|
400 | transfer_already_used | That transfer settled another refund. Send a separate one. |
404 | refund_not_found | No 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"
});
const refund = await call<Refund>('POST', '/v1/refunds/ref_7mP2wL8cYt5N/submit', {
"tx_hash": "0xabc123"
});
refund = call("POST", "/v1/refunds/ref_7mP2wL8cYt5N/submit", {
"tx_hash": "0xabc123"
})
refund, err := Call("POST", "/v1/refunds/ref_7mP2wL8cYt5N/submit", map[string]any{
"tx_hash": "0xabc123",
})
if err != nil {
return err
}
defer refund.Body.Close()
$refund = uw_call('POST', '/v1/refunds/ref_7mP2wL8cYt5N/submit', [
"tx_hash" => "0xabc123",
]);
curl -X POST "$BASE/v1/refunds/ref_7mP2wL8cYt5N/submit" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{
"tx_hash": "0xabc123"
}'
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The refund id, ref_… |
Errors
| Status | Code | When |
|---|---|---|
409 | not_cancellable | The 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');
const refund = await call<Refund>('POST', '/v1/refunds/ref_7mP2wL8cYt5N/cancel');
refund = call("POST", "/v1/refunds/ref_7mP2wL8cYt5N/cancel")
refund, err := Call("POST", "/v1/refunds/ref_7mP2wL8cYt5N/cancel", nil)
if err != nil {
return err
}
defer refund.Body.Close()
$refund = uw_call('POST', '/v1/refunds/ref_7mP2wL8cYt5N/cancel');
curl -X POST "$BASE/v1/refunds/ref_7mP2wL8cYt5N/cancel" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
limit | integer | — | Page size. Default 50, maximum 100. |
starting_after | string | — | 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');
const refunds = await call<RefundList>('GET', '/v1/refunds?limit=50');
refunds = call("GET", "/v1/refunds?limit=50")
refunds, err := Call("GET", "/v1/refunds?limit=50", nil)
if err != nil {
return err
}
defer refunds.Body.Close()
$refunds = uw_call('GET', '/v1/refunds?limit=50');
curl -X GET "$BASE/v1/refunds?limit=50" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The refund id, ref_… |
Errors
| Status | Code | When |
|---|---|---|
404 | refund_not_found | No 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');
const refund = await call<Refund>('GET', '/v1/refunds/ref_7mP2wL8cYt5N');
refund = call("GET", "/v1/refunds/ref_7mP2wL8cYt5N")
refund, err := Call("GET", "/v1/refunds/ref_7mP2wL8cYt5N", nil)
if err != nil {
return err
}
defer refund.Body.Close()
$refund = uw_call('GET', '/v1/refunds/ref_7mP2wL8cYt5N');
curl -X GET "$BASE/v1/refunds/ref_7mP2wL8cYt5N" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
Webhooks
The webhook is the truth. Register a URL, verify the signature on the raw body, and answer 2xx before you do the work.
-
DELETE
/v1/webhook-endpoints/{id}Disable an endpoint -
POST
/v1/webhook-deliveries/{id}/retryRetry a delivery -
GET
/v1/eventsList events -
POST
/v1/webhook-endpointsRegister an endpoint -
GET
/v1/webhook-endpointsList endpoints -
POST
/v1/webhook-endpoints/{id}/rotate-secretRotate the secret -
POST
/v1/webhook-endpoints/{id}/testSend a test event -
GET
/v1/webhook-deliveriesList deliveries -
POST
/v1/events/{id}/replayReplay 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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The endpoint id, whe_… |
Errors
| Status | Code | When |
|---|---|---|
403 | signature_required | A bearer token was used. Sign the request. |
404 | endpoint_not_found | No 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');
const removed = await call<WebhookEndpoint>('DELETE', '/v1/webhook-endpoints/whe_5m2Kd9');
removed = call("DELETE", "/v1/webhook-endpoints/whe_5m2Kd9")
removed, err := Call("DELETE", "/v1/webhook-endpoints/whe_5m2Kd9", nil)
if err != nil {
return err
}
defer removed.Body.Close()
$removed = uw_call('DELETE', '/v1/webhook-endpoints/whe_5m2Kd9');
curl -X DELETE "$BASE/v1/webhook-endpoints/whe_5m2Kd9" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The delivery id, whd_… |
Errors
| Status | Code | When |
|---|---|---|
404 | delivery_not_found | No 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');
const retried = await call<Delivery>('POST', '/v1/webhook-deliveries/whd_3k9Qm2/retry');
retried = call("POST", "/v1/webhook-deliveries/whd_3k9Qm2/retry")
retried, err := Call("POST", "/v1/webhook-deliveries/whd_3k9Qm2/retry", nil)
if err != nil {
return err
}
defer retried.Body.Close()
$retried = uw_call('POST', '/v1/webhook-deliveries/whd_3k9Qm2/retry');
curl -X POST "$BASE/v1/webhook-deliveries/whd_3k9Qm2/retry" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
type | string | — | 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');
const events = await call<EventList>('GET', '/v1/events?type=payment.completed');
events = call("GET", "/v1/events?type=payment.completed")
events, err := Call("GET", "/v1/events?type=payment.completed", nil)
if err != nil {
return err
}
defer events.Body.Close()
$events = uw_call('GET', '/v1/events?type=payment.completed');
curl -X GET "$BASE/v1/events?type=payment.completed" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
url | string | Yes | HTTPS URL we will POST events to. Private, loopback and metadata addresses are refused. |
events | array | — | Event types to send. Empty means all current types. The live list is GET /v1/capabilities. |
Errors
| Status | Code | When |
|---|---|---|
400 | unsafe_webhook_url | The 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"
]
});
const endpoint = await call<WebhookEndpoint>('POST', '/v1/webhook-endpoints', {
"url": "https://shop.example.com/webhooks/unifiwallet",
"events": [
"payment.completed",
"payment.expired",
"refund.completed"
]
});
endpoint = call("POST", "/v1/webhook-endpoints", {
"url": "https://shop.example.com/webhooks/unifiwallet",
"events": [
"payment.completed",
"payment.expired",
"refund.completed"
]
})
endpoint, err := Call("POST", "/v1/webhook-endpoints", map[string]any{
"url": "https://shop.example.com/webhooks/unifiwallet",
"events": []any{
"payment.completed",
"payment.expired",
"refund.completed",
},
})
if err != nil {
return err
}
defer endpoint.Body.Close()
$endpoint = uw_call('POST', '/v1/webhook-endpoints', [
"url" => "https://shop.example.com/webhooks/unifiwallet",
"events" => ["payment.completed", "payment.expired", "refund.completed"],
]);
curl -X POST "$BASE/v1/webhook-endpoints" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{
"url": "https://shop.example.com/webhooks/unifiwallet",
"events": [
"payment.completed",
"payment.expired",
"refund.completed"
]
}'
Response 201
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');
const endpoints = await call<WebhookEndpointList>('GET', '/v1/webhook-endpoints');
endpoints = call("GET", "/v1/webhook-endpoints")
endpoints, err := Call("GET", "/v1/webhook-endpoints", nil)
if err != nil {
return err
}
defer endpoints.Body.Close()
$endpoints = uw_call('GET', '/v1/webhook-endpoints');
curl -X GET "$BASE/v1/webhook-endpoints" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The endpoint id, we_… |
Errors
| Status | Code | When |
|---|---|---|
404 | endpoint_not_found | No 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');
const endpoint = await call<WebhookEndpoint>('POST', '/v1/webhook-endpoints/we_5cT1pH9sLu3W/rotate-secret');
endpoint = call("POST", "/v1/webhook-endpoints/we_5cT1pH9sLu3W/rotate-secret")
endpoint, err := Call("POST", "/v1/webhook-endpoints/we_5cT1pH9sLu3W/rotate-secret", nil)
if err != nil {
return err
}
defer endpoint.Body.Close()
$endpoint = uw_call('POST', '/v1/webhook-endpoints/we_5cT1pH9sLu3W/rotate-secret');
curl -X POST "$BASE/v1/webhook-endpoints/we_5cT1pH9sLu3W/rotate-secret" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The endpoint id, we_… |
Errors
| Status | Code | When |
|---|---|---|
404 | endpoint_not_found | No 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');
const delivery = await call<WebhookDelivery>('POST', '/v1/webhook-endpoints/we_5cT1pH9sLu3W/test');
delivery = call("POST", "/v1/webhook-endpoints/we_5cT1pH9sLu3W/test")
delivery, err := Call("POST", "/v1/webhook-endpoints/we_5cT1pH9sLu3W/test", nil)
if err != nil {
return err
}
defer delivery.Body.Close()
$delivery = uw_call('POST', '/v1/webhook-endpoints/we_5cT1pH9sLu3W/test');
curl -X POST "$BASE/v1/webhook-endpoints/we_5cT1pH9sLu3W/test" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
limit | integer | — | Page size. Default 50, maximum 100. |
starting_after | string | — | 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');
const deliveries = await call<DeliveryList>('GET', '/v1/webhook-deliveries?limit=50');
deliveries = call("GET", "/v1/webhook-deliveries?limit=50")
deliveries, err := Call("GET", "/v1/webhook-deliveries?limit=50", nil)
if err != nil {
return err
}
defer deliveries.Body.Close()
$deliveries = uw_call('GET', '/v1/webhook-deliveries?limit=50');
curl -X GET "$BASE/v1/webhook-deliveries?limit=50" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The event id, evt_… |
Errors
| Status | Code | When |
|---|---|---|
404 | event_not_found | No 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');
const event = await call<Event>('POST', '/v1/events/evt_9f3aKd2mQx7b/replay');
event = call("POST", "/v1/events/evt_9f3aKd2mQx7b/replay")
event, err := Call("POST", "/v1/events/evt_9f3aKd2mQx7b/replay", nil)
if err != nil {
return err
}
defer event.Body.Close()
$event = uw_call('POST', '/v1/events/evt_9f3aKd2mQx7b/replay');
curl -X POST "$BASE/v1/events/evt_9f3aKd2mQx7b/replay" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY"
Response 200
Account and reference
What the account can do, which assets and chains we support, and the public liveness check.
-
PATCH
/v1/account/preferencesChange how you take money -
PATCH
/v1/account/settlementChange settlement preferences -
PUT
/v1/account/accepted-assetsChange what you accept -
POST
/v1/account/domainsClaim a domain -
POST
/v1/account/domains/{domain}/verifyCheck the DNS challenge -
DELETE
/v1/account/domains/{domain}Stop trusting a domain -
GET
/v1/capabilitiesScopes and event types -
GET
/v1/assetsSupported assets -
GET
/v1/chainsSupported chains -
GET
/v1/ratesA rate between two assets -
GET
/v1/feesYour fee plan -
GET
/v1/accountThe account -
GET
/v1/audit-logAudit log -
GET
/v1/statusLiveness -
GET
/v1/status/detailOperational 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
| Name | Type | Required | Description |
|---|---|---|---|
capture_method | string | — | automatic or manual. Manual holds each payment until you capture it. |
hold_window_hours | integer | — | How long a held payment waits before the buyer may reclaim it. Capped by the platform maximum. |
gas_payer | string | — | buyer or merchant. Who carries the network fee. |
rate_mode | string | — | fixed_rate or floating_rate, for new payments. |
Errors
| Status | Code | When |
|---|---|---|
400 | manual_capture_unavailable | Holds are not switched on for this deployment. |
400 | window_above_ceiling | Longer 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"
});
const account = await call<Account>('PATCH', '/v1/account/preferences', {
"capture_method": "manual",
"hold_window_hours": 168,
"gas_payer": "merchant"
});
account = call("PATCH", "/v1/account/preferences", {
"capture_method": "manual",
"hold_window_hours": 168,
"gas_payer": "merchant"
})
account, err := Call("PATCH", "/v1/account/preferences", map[string]any{
"capture_method": "manual",
"hold_window_hours": 168,
"gas_payer": "merchant",
})
if err != nil {
return err
}
defer account.Body.Close()
$account = uw_call('PATCH', '/v1/account/preferences', [
"capture_method" => "manual",
"hold_window_hours" => 168,
"gas_payer" => "merchant",
]);
curl -X PATCH "$BASE/v1/account/preferences" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{
"capture_method": "manual",
"hold_window_hours": 168,
"gas_payer": "merchant"
}'
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
destination_id | string | — | A verified, active destination on this account. |
asset | string | — | Settlement asset symbol. |
chain_id | integer | — | Settlement chain. |
mode | string | — | convert_all, keep_native, split or per_asset. |
max_slippage_bps | integer | — | Conversion slippage bound, in basis points. |
Errors
| Status | Code | When |
|---|---|---|
400 | unknown_destination | No destination with that id on this account. |
400 | destination_not_verified | Verify the destination before making it the default. |
403 | signature_required | A bearer token was used. Sign the request. |
403 | panel_only | Per-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
});
const account = await call<Account>('PATCH', '/v1/account/settlement', {
"destination_id": "dst_9k2Qm",
"asset": "USDC",
"chain_id": 8453
});
account = call("PATCH", "/v1/account/settlement", {
"destination_id": "dst_9k2Qm",
"asset": "USDC",
"chain_id": 8453
})
account, err := Call("PATCH", "/v1/account/settlement", map[string]any{
"destination_id": "dst_9k2Qm",
"asset": "USDC",
"chain_id": 8453,
})
if err != nil {
return err
}
defer account.Body.Close()
$account = uw_call('PATCH', '/v1/account/settlement', [
"destination_id" => "dst_9k2Qm",
"asset" => "USDC",
"chain_id" => 8453,
]);
curl -X PATCH "$BASE/v1/account/settlement" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{
"destination_id": "dst_9k2Qm",
"asset": "USDC",
"chain_id": 8453
}'
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
mode | string | — | all_supported or allowlist. |
chains_allowed | array | — | Chain ids, when the mode is allowlist. |
assets_denied | array | — | Asset symbols to refuse. |
min_payment_usd | string | — | Below this a payment costs more to collect than it is worth. |
underpayment_tolerance_bps | integer | — | 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"
]
});
const account = await call<Account>('PUT', '/v1/account/accepted-assets', {
"mode": "allowlist",
"chains_allowed": [
8453
],
"assets_denied": [
"DAI"
]
});
account = call("PUT", "/v1/account/accepted-assets", {
"mode": "allowlist",
"chains_allowed": [
8453
],
"assets_denied": [
"DAI"
]
})
account, err := Call("PUT", "/v1/account/accepted-assets", map[string]any{
"mode": "allowlist",
"chains_allowed": []any{
8453,
},
"assets_denied": []any{
"DAI",
},
})
if err != nil {
return err
}
defer account.Body.Close()
$account = uw_call('PUT', '/v1/account/accepted-assets', [
"mode" => "allowlist",
"chains_allowed" => [8453],
"assets_denied" => ["DAI"],
]);
curl -X PUT "$BASE/v1/account/accepted-assets" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{
"mode": "allowlist",
"chains_allowed": [
8453
],
"assets_denied": [
"DAI"
]
}'
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
domain | string | Yes | The hostname, without a scheme. |
Errors
| Status | Code | When |
|---|---|---|
403 | signature_required | A 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"
});
const claim = await call<Domain>('POST', '/v1/account/domains', {
"domain": "shop.example.com"
});
claim = call("POST", "/v1/account/domains", {
"domain": "shop.example.com"
})
claim, err := Call("POST", "/v1/account/domains", map[string]any{
"domain": "shop.example.com",
})
if err != nil {
return err
}
defer claim.Body.Close()
$claim = uw_call('POST', '/v1/account/domains', [
"domain" => "shop.example.com",
]);
curl -X POST "$BASE/v1/account/domains" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{
"domain": "shop.example.com"
}'
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
domain | string | Yes | The hostname you claimed. |
Errors
| Status | Code | When |
|---|---|---|
400 | domain_not_verified | The TXT record is missing or does not match. DNS may not have propagated yet. |
400 | domain_not_claimed | Claim 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');
const verified = await call<Domain>('POST', '/v1/account/domains/shop.example.com/verify');
verified = call("POST", "/v1/account/domains/shop.example.com/verify")
verified, err := Call("POST", "/v1/account/domains/shop.example.com/verify", nil)
if err != nil {
return err
}
defer verified.Body.Close()
$verified = uw_call('POST', '/v1/account/domains/shop.example.com/verify');
curl -X POST "$BASE/v1/account/domains/shop.example.com/verify" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
domain | string | Yes | The 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');
const removed = await call<Domain>('DELETE', '/v1/account/domains/shop.example.com');
removed = call("DELETE", "/v1/account/domains/shop.example.com")
removed, err := Call("DELETE", "/v1/account/domains/shop.example.com", nil)
if err != nil {
return err
}
defer removed.Body.Close()
$removed = uw_call('DELETE', '/v1/account/domains/shop.example.com');
curl -X DELETE "$BASE/v1/account/domains/shop.example.com" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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');
const capabilities = await call<Capabilities>('GET', '/v1/capabilities');
capabilities = call("GET", "/v1/capabilities")
capabilities, err := Call("GET", "/v1/capabilities", nil)
if err != nil {
return err
}
defer capabilities.Body.Close()
$capabilities = uw_call('GET', '/v1/capabilities');
curl -X GET "$BASE/v1/capabilities" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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');
const assets = await call<AssetList>('GET', '/v1/assets');
assets = call("GET", "/v1/assets")
assets, err := Call("GET", "/v1/assets", nil)
if err != nil {
return err
}
defer assets.Body.Close()
$assets = uw_call('GET', '/v1/assets');
curl -X GET "$BASE/v1/assets" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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');
const chains = await call<ChainList>('GET', '/v1/chains');
chains = call("GET", "/v1/chains")
chains, err := Call("GET", "/v1/chains", nil)
if err != nil {
return err
}
defer chains.Body.Close()
$chains = uw_call('GET', '/v1/chains');
curl -X GET "$BASE/v1/chains" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
from | string | Yes | Source asset or fiat code. |
to | string | Yes | Destination asset or fiat code. |
Errors
| Status | Code | When |
|---|---|---|
400 | missing_params | from 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');
const rate = await call<Rate>('GET', '/v1/rates?from=USD&to=USDC');
rate = call("GET", "/v1/rates?from=USD&to=USDC")
rate, err := Call("GET", "/v1/rates?from=USD&to=USDC", nil)
if err != nil {
return err
}
defer rate.Body.Close()
$rate = uw_call('GET', '/v1/rates?from=USD&to=USDC');
curl -X GET "$BASE/v1/rates?from=USD&to=USDC" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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');
const fees = await call<FeePlan>('GET', '/v1/fees');
fees = call("GET", "/v1/fees")
fees, err := Call("GET", "/v1/fees", nil)
if err != nil {
return err
}
defer fees.Body.Close()
$fees = uw_call('GET', '/v1/fees');
curl -X GET "$BASE/v1/fees" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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');
const account = await call<Account>('GET', '/v1/account');
account = call("GET", "/v1/account")
account, err := Call("GET", "/v1/account", nil)
if err != nil {
return err
}
defer account.Body.Close()
$account = uw_call('GET', '/v1/account');
curl -X GET "$BASE/v1/account" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
limit | integer | — | Page size. Default 50, maximum 100. |
starting_after | string | — | 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');
const entries = await call<AuditList>('GET', '/v1/audit-log?limit=50');
entries = call("GET", "/v1/audit-log?limit=50")
entries, err := Call("GET", "/v1/audit-log?limit=50", nil)
if err != nil {
return err
}
defer entries.Body.Close()
$entries = uw_call('GET', '/v1/audit-log?limit=50');
curl -X GET "$BASE/v1/audit-log?limit=50" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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');
const status = await call<Status>('GET', '/v1/status');
status = call("GET", "/v1/status")
status, err := Call("GET", "/v1/status", nil)
if err != nil {
return err
}
defer status.Body.Close()
$status = uw_call('GET', '/v1/status');
curl -X GET "$BASE/v1/status" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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');
const detail = await call<StatusDetail>('GET', '/v1/status/detail');
detail = call("GET", "/v1/status/detail")
detail, err := Call("GET", "/v1/status/detail", nil)
if err != nil {
return err
}
defer detail.Body.Close()
$detail = uw_call('GET', '/v1/status/detail');
curl -X GET "$BASE/v1/status/detail" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
Test mode
Drive a test payment to any outcome with no wallet and no chain. Refused outright for a live credential.
-
GET
/v1/test/outcomesList simulatable outcomes -
POST
/v1/test/payments/{id}/simulateSimulate 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
| Status | Code | When |
|---|---|---|
403 | test_mode_only | Simulation 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');
const outcomes = await call<OutcomeList>('GET', '/v1/test/outcomes');
outcomes = call("GET", "/v1/test/outcomes")
outcomes, err := Call("GET", "/v1/test/outcomes", nil)
if err != nil {
return err
}
defer outcomes.Body.Close()
$outcomes = uw_call('GET', '/v1/test/outcomes');
curl -X GET "$BASE/v1/test/outcomes" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE"
Response 200
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
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | A test-mode payment id, pay_… |
Body
| Name | Type | Required | Description |
|---|---|---|---|
outcome | string | — | One of the values from GET /v1/test/outcomes. Defaults to completed. |
Errors
| Status | Code | When |
|---|---|---|
403 | test_mode_only | Simulation is available to test-mode credentials only. |
404 | payment_not_found | No 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"
});
const payment = await call<Payment>('POST', '/v1/test/payments/pay_3nQ8xK2mLp4R/simulate', {
"outcome": "underpaid"
});
payment = call("POST", "/v1/test/payments/pay_3nQ8xK2mLp4R/simulate", {
"outcome": "underpaid"
})
payment, err := Call("POST", "/v1/test/payments/pay_3nQ8xK2mLp4R/simulate", map[string]any{
"outcome": "underpaid",
})
if err != nil {
return err
}
defer payment.Body.Close()
$payment = uw_call('POST', '/v1/test/payments/pay_3nQ8xK2mLp4R/simulate', [
"outcome" => "underpaid",
]);
curl -X POST "$BASE/v1/test/payments/pay_3nQ8xK2mLp4R/simulate" \
-H "Content-Type: application/json" \
-H "X-UW-Org-Id: $UW_ORG_ID" \
-H "X-UW-Key-Id: $UW_KEY_ID" \
-H "X-UW-Timestamp: $TIMESTAMP" \
-H "X-UW-Nonce: $NONCE" \
-H "X-UW-Signature: v1=$SIGNATURE" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{
"outcome": "underpaid"
}'
Response 200
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.