# Issue an additional API key
Source: https://docs.useroutr.com/api-reference/applications/issue-an-additional-api-key
/api-reference/openapi.json post /v1/applications/{id}/api-keys
Issues a new key for rotation. The secret is returned only in this response. An optional Idempotency-Key replays the original result instead of issuing another key.
# List the application's API keys
Source: https://docs.useroutr.com/api-reference/applications/list-the-applications-api-keys
/api-reference/openapi.json get /v1/applications/{id}/api-keys
Newest first. Secrets are never included.
# Operational overview of the application
Source: https://docs.useroutr.com/api-reference/applications/operational-overview-of-the-application
/api-reference/openapi.json get /v1/applications/{id}/overview
Counts of funding intents and settlements by state, posted credit totals per asset, and webhook delivery health, computed by the API.
# Remove the application's logo
Source: https://docs.useroutr.com/api-reference/applications/remove-the-applications-logo
/api-reference/openapi.json delete /v1/applications/{id}/logo
The checkout shows the application's initial instead. Removing a logo that is not set succeeds.
# Retrieve the application the request acts for
Source: https://docs.useroutr.com/api-reference/applications/retrieve-the-application-the-request-acts-for
/api-reference/openapi.json get /v1/applications/me
Identifies the caller's application and, for API-key requests, the key used (never its secret). For dashboard sessions `api_key` is null.
# Retrieve the authenticated application
Source: https://docs.useroutr.com/api-reference/applications/retrieve-the-authenticated-application
/api-reference/openapi.json get /v1/applications/{id}
# Revoke an API key
Source: https://docs.useroutr.com/api-reference/applications/revoke-an-api-key
/api-reference/openapi.json post /v1/applications/{id}/api-keys/{keyId}/revoke
# Update application configuration
Source: https://docs.useroutr.com/api-reference/applications/update-application-configuration
/api-reference/openapi.json patch /v1/applications/{id}
# Upload the application's logo
Source: https://docs.useroutr.com/api-reference/applications/upload-the-applications-logo
/api-reference/openapi.json put /v1/applications/{id}/logo
The request body is the image itself, sent with `Content-Type` `image/png`, `image/jpeg` or `image/webp`. The logo must be a still image of at most 256 KB, between 64 and 1024 pixels on each side. Its type is read from the file, not from the header. Uploading the logo the application already has changes nothing. The logo appears on the hosted checkout and in `branding.logo_url`.
# List supported assets
Source: https://docs.useroutr.com/api-reference/assets/list-supported-assets
/api-reference/openapi.json get /v1/assets
Stellar assets a funding intent can require, with their precision and Stellar identity, followed by assets on other networks a payer may bring as a quote's source (their network and token contract). Only Stellar assets can be a destination.
# Commit to paying with one asset
Source: https://docs.useroutr.com/api-reference/checkout/commit-to-paying-with-one-asset
/api-reference/openapi.json post /v1/public/funding-intents/{token}/payment-option
Locks a route for the chosen asset and switches the intent's funding instructions to it, fixing the exact amount the payer must send and where. Returns the refreshed payer view, so a checkout reads the instructions from the same shape it already polls. One intent can hold one lock: selecting again while a quote is locked answers 409.
# List the ways a payer may fund this intent
Source: https://docs.useroutr.com/api-reference/checkout/list-the-ways-a-payer-may-fund-this-intent
/api-reference/openapi.json get /v1/public/funding-intents/{token}/payment-options
Every asset the payer may send, priced by the routing engine: what they would send, the fees that would be charged including Useroutr's own, and how long the route usually takes. Nothing here is binding: the amounts are estimates until an option is selected. Assets that cannot be routed are returned as unavailable with a reason rather than omitted. Requires no credential; the funding page token is the capability.
# Retrieve the payer's view of a funding intent
Source: https://docs.useroutr.com/api-reference/checkout/retrieve-the-payers-view-of-a-funding-intent
/api-reference/openapi.json get /v1/public/funding-intents/{token}
Public, token-addressed view used by the hosted funding page: what to pay, where, and the current status. Requires no credential; the token is unguessable and is returned to the application as `hosted_funding_url`.
# Create a funding intent
Source: https://docs.useroutr.com/api-reference/funding-intents/create-a-funding-intent
/api-reference/openapi.json post /v1/funding-intents
Creates a funding intent and registers it with the Useroutr protocol on Stellar. Requires an Idempotency-Key header; repeating a request with the same key and body returns the original result.
# List funding intents
Source: https://docs.useroutr.com/api-reference/funding-intents/list-funding-intents
/api-reference/openapi.json get /v1/funding-intents
Newest first; filter by `status` and exact `reference`. Pass `cursor` from a previous response to fetch the next page.
# List payments observed for a funding intent
Source: https://docs.useroutr.com/api-reference/funding-intents/list-payments-observed-for-a-funding-intent
/api-reference/openapi.json get /v1/funding-intents/{id}/payments
Stellar payments attributed to the funding intent, including rejected attempts, in detection order. At most `limit` payments are returned.
# Retrieve a funding intent
Source: https://docs.useroutr.com/api-reference/funding-intents/retrieve-a-funding-intent
/api-reference/openapi.json get /v1/funding-intents/{id}
# API reference
Source: https://docs.useroutr.com/api-reference/introduction
Every endpoint, with a Node.js SDK example and a cURL example beside it.
The Useroutr API is JSON over HTTPS with bearer authentication. Each endpoint page shows the
[Node.js SDK](/sdks/typescript) call first and the raw HTTP request second, and you can try any
request against the sandbox from the page.
| Environment | Base URL | Keys |
| - | - | - |
| Sandbox | `https://api.sandbox.useroutr.com` | `ur_test_...` |
| Production | `https://api.useroutr.com` | `ur_live_...` |
## Conventions
`Authorization: Bearer ur_test_...`, from your server only.
An `Idempotency-Key` makes money-moving requests safe to retry. Required to create a funding
intent.
One envelope, a stable `code`, a `requestId` and a `retryable` flag.
Newest first, with `limit` and a `next_cursor`.
## Data
* **Amounts** are decimal strings such as `"25.0000000"`, never floats, and always travel with
their asset.
* **Assets** are ids such as `asset_usdc_stellar`. See [Assets and networks](/concepts/assets-and-networks).
* **Ids** are prefixed by type: `fi_` funding intents, `qt_` quotes, `pay_` payments, `stl_`
settlements, `ref_` refunds, `evt_` events, `whep_` webhook endpoints.
* **Timestamps** are ISO 8601 in UTC.
## Checkout endpoints
The three endpoints under `/v1/public/funding-intents/{token}` power the checkout. They take no
API key: the funding token is the credential, and they answer any origin so a browser can call
them directly. Use them to build a [custom checkout](/payments/custom-checkout).
## OpenAPI
The API describes itself as OpenAPI 3.1 at `/openapi.json` on each base URL, for generating a
client in another language.
# List the application's balances
Source: https://docs.useroutr.com/api-reference/ledger/list-the-applications-balances
/api-reference/openapi.json get /v1/applications/{id}/balances
Balances are projections of posted ledger entries, one per asset.
# List the application's ledger entries
Source: https://docs.useroutr.com/api-reference/ledger/list-the-applications-ledger-entries
/api-reference/openapi.json get /v1/applications/{id}/ledger
Newest first. Pass `cursor` from a previous response to fetch the next page.
# List payments attributed to the application's funding intents
Source: https://docs.useroutr.com/api-reference/payments/list-payments-attributed-to-the-applications-funding-intents
/api-reference/openapi.json get /v1/payments
Newest first; filter by `status` and `funding_intent_id`. Pass `cursor` from a previous response to fetch the next page.
# List quotes of a funding intent
Source: https://docs.useroutr.com/api-reference/quotes/list-quotes-of-a-funding-intent
/api-reference/openapi.json get /v1/funding-intents/{id}/quotes
Newest first, cursor-paginated.
# Quote a route for funding the intent from a source asset
Source: https://docs.useroutr.com/api-reference/quotes/quote-a-route-for-funding-the-intent-from-a-source-asset
/api-reference/openapi.json post /v1/funding-intents/{id}/quotes
Discovers, validates, scores and selects the best route from the given source asset to the intent's destination requirement, and returns it as a quote. The destination comes from the funding intent; only the source is chosen here. By default (`type` EXACT_OUTPUT) the intent's requirement is fixed and `source.amount` in the response is what the payer must send so that it arrives. With `type` EXACT_INPUT the request's `source.amount` fixes what the payer sends and the response's `destination.amount` is what the route delivers, which must cover the requirement; anything delivered above it is refunded to the payer. An optional Idempotency-Key replays the original result.
# Retrieve a quote
Source: https://docs.useroutr.com/api-reference/quotes/retrieve-a-quote
/api-reference/openapi.json get /v1/quotes/{id}
The quote with its route, fees, lock and execution state.
# Select a quote and lock its route
Source: https://docs.useroutr.com/api-reference/quotes/select-a-quote-and-lock-its-route
/api-reference/openapi.json post /v1/quotes/{id}/select
Locks the quote's route for the lock period: the intent's funding instructions switch to the quote's source asset and amount, and the deposit account is prepared to receive it. Execution starts automatically once the payment is confirmed; there is no separate execute call. Expired quotes are refused.
# Give a pending refund its destination
Source: https://docs.useroutr.com/api-reference/refunds/give-a-pending-refund-its-destination
/api-reference/openapi.json patch /v1/refunds/{id}
Points a PENDING refund at a Stellar account. A refund created automatically for a payment whose payer is not a Stellar account (a bridge contract, an EVM address) has no destination and does not pay out until one is supplied here.
# Request a refund of a payment that did not satisfy the intent
Source: https://docs.useroutr.com/api-reference/refunds/request-a-refund-of-a-payment-that-did-not-satisfy-the-intent
/api-reference/openapi.json post /v1/funding-intents/{id}/payments/{paymentId}/refunds
Records a refund for whatever of the payment has not already been refunded, paid out by a worker from the deposit account that received it. The destination defaults to the payer's account. One refund can be in progress per payment (409 while one is), and a payment that has been refunded in full is refused (422). An optional Idempotency-Key replays the original result.
# Retrieve a refund
Source: https://docs.useroutr.com/api-reference/refunds/retrieve-a-refund
/api-reference/openapi.json get /v1/refunds/{id}
# List request logs
Source: https://docs.useroutr.com/api-reference/request-logs/list-request-logs
/api-reference/openapi.json get /v1/request-logs
Requests made with the application's API keys in the last 30 days, newest first. Metadata only: bodies, headers and query strings are never recorded. Filter by `outcome`, `request_id` and `api_key_id`.
# Retrieve a request log
Source: https://docs.useroutr.com/api-reference/request-logs/retrieve-a-request-log
/api-reference/openapi.json get /v1/request-logs/{id}
# List settlements
Source: https://docs.useroutr.com/api-reference/settlements/list-settlements
/api-reference/openapi.json get /v1/settlements
Newest first; filter by `status` and `funding_intent_id`. Pass `cursor` from a previous response to fetch the next page.
# Retrieve a settlement
Source: https://docs.useroutr.com/api-reference/settlements/retrieve-a-settlement
/api-reference/openapi.json get /v1/settlements/{id}
# Retrieve the settlement of a funding intent
Source: https://docs.useroutr.com/api-reference/settlements/retrieve-the-settlement-of-a-funding-intent
/api-reference/openapi.json get /v1/funding-intents/{id}/settlement
# Create a webhook endpoint
Source: https://docs.useroutr.com/api-reference/webhooks/create-a-webhook-endpoint
/api-reference/openapi.json post /v1/webhooks/endpoints
Registers an HTTPS endpoint and returns its signing secret. The secret is returned only in this response. An optional Idempotency-Key replays the original result.
# Delete a webhook endpoint
Source: https://docs.useroutr.com/api-reference/webhooks/delete-a-webhook-endpoint
/api-reference/openapi.json delete /v1/webhooks/endpoints/{id}
Pending deliveries to the endpoint are cancelled; delivered history is kept.
# List webhook deliveries
Source: https://docs.useroutr.com/api-reference/webhooks/list-webhook-deliveries
/api-reference/openapi.json get /v1/webhooks/deliveries
Newest first; filter by `event_id`, `endpoint_id` and `status`.
# List webhook endpoints
Source: https://docs.useroutr.com/api-reference/webhooks/list-webhook-endpoints
/api-reference/openapi.json get /v1/webhooks/endpoints
Newest first. Pass `cursor` from a previous response to fetch the next page.
# List webhook events
Source: https://docs.useroutr.com/api-reference/webhooks/list-webhook-events
/api-reference/openapi.json get /v1/webhooks/events
Every event generated for the application, whether or not an endpoint was subscribed. Newest first; filter by `type` and `funding_intent_id`.
# Retrieve a webhook delivery with its attempt history
Source: https://docs.useroutr.com/api-reference/webhooks/retrieve-a-webhook-delivery-with-its-attempt-history
/api-reference/openapi.json get /v1/webhooks/deliveries/{id}
# Retrieve a webhook endpoint
Source: https://docs.useroutr.com/api-reference/webhooks/retrieve-a-webhook-endpoint
/api-reference/openapi.json get /v1/webhooks/endpoints/{id}
# Retrieve a webhook event
Source: https://docs.useroutr.com/api-reference/webhooks/retrieve-a-webhook-event
/api-reference/openapi.json get /v1/webhooks/events/{id}
# Retry a failed or exhausted delivery
Source: https://docs.useroutr.com/api-reference/webhooks/retry-a-failed-or-exhausted-delivery
/api-reference/openapi.json post /v1/webhooks/deliveries/{id}/retry
Re-arms the delivery with a fresh attempt budget. Deliveries that are queued or already delivered are returned unchanged.
# Rotate a webhook endpoint's signing secret
Source: https://docs.useroutr.com/api-reference/webhooks/rotate-a-webhook-endpoints-signing-secret
/api-reference/openapi.json post /v1/webhooks/endpoints/{id}/rotate-secret
Issues a new secret, returned only in this response. Deliveries are signed with both the new and the previous secret during the rotation grace period.
# Update a webhook endpoint
Source: https://docs.useroutr.com/api-reference/webhooks/update-a-webhook-endpoint
/api-reference/openapi.json patch /v1/webhooks/endpoints/{id}
Changes the URL, subscriptions, description, or enables and disables delivery.
# Changelog
Source: https://docs.useroutr.com/changelog
What is new in the Useroutr API, SDKs and dashboard.
## Refunds on EVM networks
Payments made to a deposit address on Base, Arbitrum or Ethereum are now refunded on that
network when they cannot fund the intent, or when something is left over; see
[Refunds](/guides/refunds). A refund's `destination.network` names the network it is paid on.
A payment recorded for such a refund can have a null `transaction_hash` and `payer_address`
when no single transaction or sender can be named.
## Official SDKs
`@useroutr/sdk` for Node.js and `@useroutr/checkout-react` for React are the recommended way to
integrate. The SDK covers every endpoint in this reference.
## Request logs
Every request made with an API key is now logged for 30 days, including requests rejected by
validation. Find them under **Developers → Logs** in the dashboard, or with
`useroutr.requestLogs.list()`. Bodies, headers and query strings are never recorded.
## Branding
Upload a logo and set a brand colour, in the dashboard or with the SDK, and every checkout wears
them. The embedded checkout can override them per page with `appearance`.
## A new dashboard
Home now shows a setup guide, what was credited and how far your funding intents get. Funding
intents open beside the list, the create page previews the checkout as you type, and assets are
shown by their logo and network.
# Assets and networks
Source: https://docs.useroutr.com/concepts/assets-and-networks
How Useroutr identifies an asset, and which assets can pay and be paid.
An asset is identified by its network and, on Stellar, its issuer, never by its symbol alone.
Two tokens called USDC from different issuers are different assets, and Useroutr treats them
that way. Each asset has an id, and the id is what you send in every request.
```ts theme={null}
const { data: assets } = await useroutr.assets.list();
```
```json theme={null}
{
"id": "asset_usdc_stellar",
"object": "asset",
"network": "stellar",
"symbol": "USDC",
"decimals": 7,
"stellar_asset": "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
"code": "USDC",
"issuer": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
"contract": "C..."
}
```
`symbol` is for display. `decimals` is the asset's precision: an amount may have no more
decimal places than that. The catalog differs between the sandbox and production, so read it
from the API rather than hard-coding a list.
## Stellar assets
Every funding intent settles on Stellar, so the asset you ask for is a Stellar asset. Native XLM
has `stellar_asset: "native"` and no issuer; an issued asset such as USDC names its issuer.
When you receive an issued asset, your destination account must hold a trustline for it before
you create the intent. Useroutr checks this up front and refuses the intent with
`INVALID_DESTINATION` rather than taking a payment it could not deliver.
## Assets on other networks
The catalog can also list assets on EVM networks, such as USDC or WETH on Base. Your users can
**pay** with these: a [quote](/concepts/quotes-and-routes) bridges the payment to Stellar and
converts it into the asset you asked for, and the payer sends one token transfer to a deposit
address on their own network.
You can also ask to **receive** an asset on another network, such as USDC on Base. Your users
can then pay from Stellar or from any other network in the catalog: the payment is bridged to
Stellar, converted if needed, and bridged out to your address. A user paying in that same asset
on that same network pays you directly: their payment is forwarded to your address there,
without passing through Stellar. The payment then has `network` set to that network, amounts at
the token's own precision, and no `operation_id`.
These entries have `network` set to the EVM network and name their token `contract` instead of
an issuer.
## Amounts
Amounts are always decimal strings, such as `"25"` or `"12.5000000"`, never floats. The API
returns them at the asset's full precision. An amount never travels without its asset: every
response that carries an amount names the asset it is in.
# Balances and ledger
Source: https://docs.useroutr.com/concepts/balances-and-ledger
The accounting record of value that has verifiably reached you.
Your ledger is the record of every value that has verifiably reached your destination. It lives
in Useroutr's database; blockchain transactions are the evidence behind each entry, and the
ledger is the account.
Useroutr holds no funds for you. Your balance describes what has already settled to your own
destination account.
## One intent, one credit
A successful funding intent results in exactly one posted `CREDIT`. Unique constraints in the
database enforce this, so a replayed webhook, a retried job or a repeated call can never post a
second one, and reconciliation flags any drift.
## Balances
One balance per asset, projected from posted entries.
```ts theme={null}
const { data: balances } = await useroutr.balances.list("app_5c1e0a9f");
```
## Ledger entries
Newest first, cursor paginated.
```ts theme={null}
for await (const entry of useroutr.ledger.iterate("app_5c1e0a9f")) {
if (entry.type === "CREDIT" && entry.status === "POSTED") {
console.log(entry.amount, entry.asset, entry.funding_intent_id);
}
}
```
| Field | Values |
| - | - |
| `type` | `CREDIT`, `REFUND` or `ADJUSTMENT` |
| `direction` | `CREDIT` or `DEBIT` |
| `status` | `PENDING`, `POSTED` or `VOIDED`. Only `POSTED` counts. |
| `amount` | A decimal string in `asset` |
Each entry links to the funding intent, payment, settlement or refund it came from. Entries are
never edited: a correction is a new, compensating entry.
Your application id is on `useroutr.applications.current()`, and in the dashboard under
**Settings → Application**.
# Funding intents
Source: https://docs.useroutr.com/concepts/funding-intents
A request for value, and the lifecycle it moves through until you are credited.
A funding intent is your application's request to receive an exact amount of an exact asset at
an exact destination. It is registered on the Useroutr Soroban protocol when you create it, and
it only ever moves forward through a fixed lifecycle.
## Create one
```ts theme={null}
const intent = await useroutr.fundingIntents.create(
{
amount: "25",
asset: "asset_usdc_stellar",
reference: "ORDER-1042",
metadata: { plan: "pro", seats: 5 },
},
{ idempotencyKey: "ORDER-1042" },
);
```
A positive decimal string within the asset's precision, for example `"25"` or `"12.5"`. Never a
float.
An asset id from `useroutr.assets.list()`, such as `asset_usdc_stellar`. See
[Assets and networks](/concepts/assets-and-networks).
Where the value settles: `{ network, asset, address }`. Defaults to your application's default
destination. It must be on the asset's own network and for the same asset. A Stellar account
must already exist and, for an issued asset such as USDC, hold a trustline for it; otherwise the
request is refused with `INVALID_DESTINATION` rather than taking a payment that could not be
delivered.
Your own id, up to 255 characters, returned unchanged. Your order id is a good choice.
Up to 50 keys of strings, numbers, booleans or null, returned unchanged.
ISO 8601. Defaults to 30 minutes from now, at most 24 hours.
The idempotency key is required. The same key with the same body returns the original intent;
the same key with a different body is refused. See [Idempotency](/integrate/idempotency).
## Lifecycle
| Status | What it means | Webhook |
| - | - | - |
| `CREATED` | Created; protocol registration in flight | `funding.created` |
| `AWAITING_PAYMENT` | Registered; `funding_instructions` are ready to show | |
| `PARTIALLY_FUNDED` | Payments so far are below the amount; instructions ask for the rest | `funding.partially_funded` |
| `PAYMENT_CONFIRMED` | A valid payment covering the amount was verified | `funding.payment_confirmed` |
| `SETTLING` | A settlement is being driven on the protocol | `funding.settling` |
| `SETTLED` | The settlement was confirmed against chain state | `funding.settled` |
| `CREDITED` | The ledger credit is posted. You have been paid. | `funding.credited` |
| `EXPIRED` | `expires_at` passed before it was funded. Set once Useroutr has checked the chain up to `expires_at`, so a payment made in time is never lost to expiry. | `funding.expired` |
| `FAILED` | Registration or settlement failed; see `failure` | `funding.failed` |
`PAYMENT_DETECTED` and `ROUTING` happen inside the steps that confirm a payment and create a
settlement, so you never observe them. `CANCELLED`, `REFUND_PENDING` and `REFUNDED` are defined
for flows that are not live yet; no intent reaches them today.
Fulfil orders on `CREDITED`, never earlier. It is the only status that means the value is yours.
## Funding instructions
Every intent gets a **deposit account**: a Stellar account Useroutr creates for that intent
alone. It is not your destination; your destination receives the value once the payment
settles.
While the intent is `AWAITING_PAYMENT` or `PARTIALLY_FUNDED`, `funding_instructions` tells your
user exactly what to send:
```json theme={null}
{
"network": "stellar",
"stellar_network": "testnet",
"deposit": { "address": "GCLPHLWOSOZNS7T2CPP6KA36V6QKMLXZETKPTY523PIQTOWL7IMWP6IH", "type": "account" },
"asset": { "id": "asset_xlm_stellar", "symbol": "XLM", "stellar_asset": "native", "decimals": 7 },
"amount": "10.0000000",
"required_amount": "10.0000000",
"received_amount": "0.0000000",
"expires_at": "2026-09-26T11:53:53.000Z"
}
```
No memo is needed: the address alone identifies the intent.
## Partial payments and overpayments
Payments add up. A smaller payment moves the intent to `PARTIALLY_FUNDED` and the instructions
ask for the remainder. Once the total reaches the amount, the intent is `PAYMENT_CONFIRMED`.
Anything above the amount is never credited. It is recorded as a refund with reason
`OVERPAYMENT` and paid back to the payer. Payments that do not match, such as the wrong asset or
a payment after expiry, are recorded as rejected and fire `payment.rejected`, and the intent
keeps waiting. See [Refunds](/guides/refunds).
## Read
```ts theme={null}
const intent = await useroutr.fundingIntents.retrieve("fi_2d6a91c4");
const { data: payments } = await useroutr.fundingIntents.listPayments(intent.id);
for await (const credited of useroutr.fundingIntents.iterate({ status: "CREDITED" })) {
console.log(credited.reference);
}
```
The intent carries its `payment` and `settlement` once they exist, `hosted_funding_url` for the
payer, and while a quote is locked, its `source`, `quote` and `route`.
# Quotes and routes
Source: https://docs.useroutr.com/concepts/quotes-and-routes
Price what your user sends in the asset they hold, and lock the route that delivers it.
A funding intent says what you need. A quote says, for the asset your user holds, how much they
send and which route turns it into your asset. The destination is always the intent's; a quote
only chooses the source.
If your user already holds the asset you asked for, skip quotes entirely: the intent is payable
as it is.
## Quote, then lock
```ts theme={null}
const quote = await useroutr.quotes.create(intent.id, {
source: { network: "stellar", asset: "asset_xlm_stellar" },
});
console.log(quote.source.amount); // the most your user sends
console.log(quote.fees.total); // every fee, itemised in quote.fees.items
await useroutr.quotes.select(quote.id);
```
Selecting locks the route. From then on the intent's `funding_instructions` ask for the quote's
asset and amount, and the hosted checkout shows the same. There is no execute call: the route
runs automatically once the payment is confirmed, exactly as quoted. A moving market never
re-prices a locked quote.
## Two kinds of quote
Your amount is fixed. The quote says the most your user sends, including slippage.
`source.expected_amount` is what the route should use, and anything unused is refunded to your
user after the swap.
```ts theme={null}
await useroutr.quotes.create(intent.id, {
source: { network: "stellar", asset: "asset_xlm_stellar" },
});
```
Your user fixes what they send and the route decides what arrives. The guaranteed delivery must
still cover your amount, or the quote is refused with `INSUFFICIENT_INPUT`. You are credited
exactly your amount; anything above it goes back to your user.
```ts theme={null}
await useroutr.quotes.create(intent.id, {
type: "EXACT_INPUT",
source: { network: "stellar", asset: "asset_xlm_stellar", amount: "48" },
});
```
## Routes
| Route | When |
| - | - |
| `DIRECT_STELLAR` | Your user pays in the asset you asked for. Costs nothing. |
| `STELLAR_SWAP` | A different Stellar asset, swapped inside the intent's deposit account |
| `CROSS_CHAIN` | An asset bridged between Stellar and another network |
| `DIRECT_EVM` | Your user pays in the asset you asked for, on the same network you receive on, such as USDC on Base to USDC on Base. It is forwarded to you on that network. Costs nothing. |
| `COMPOSED` | Two or three legs through Stellar, when no single route reaches your asset: a bridge and a swap, or a bridge in and a bridge out with a swap between them if needed |
Your user always sees one route, one amount and one deposit address, whatever happens behind it.
## Fees and slippage
`fees` lists every fee by category (`NETWORK`, `PROVIDER`, `SWAP`, `BRIDGE`, `USEROUTR`) with a
`total` in the source asset. Useroutr adds nothing to the rate: a pool's own fee is part of its
price and shows in `expected_amount`. `slippage_bps` applies to swaps only and defaults to 100
(1%); pass 1 to 5000 to change it.
## Expiry and locks
A quote can be selected until `expires_at`. Once locked, the lock lasts until
`lock_expires_at` if nothing arrives; after a payment arrives the lock holds until the route
finishes. If a lock lapses unpaid, the intent goes back to its own asset and you can quote
again.
| Error | Meaning |
| - | - |
| `NO_ROUTE_AVAILABLE` | No route can deliver this pair |
| `INSUFFICIENT_INPUT` | An exact input buys less than your amount |
| `QUOTE_EXPIRED` | The quote passed `expires_at` |
| `QUOTE_NOT_SELECTABLE` | The intent cannot take a quote now, for example it is already paid |
| `CONFLICT` | Another quote is already locked for this intent |
# Settlement
Source: https://docs.useroutr.com/concepts/settlement
How a verified payment reaches your destination, and why you can trust the credit that follows.
A settlement moves a verified payment's value from the intent's deposit account to your
destination, through the Useroutr settlement contract on Soroban. It is the evidence every credit
rests on: no settlement, no credit.
## Lifecycle
| Status | What it means |
| - | - |
| `PENDING` | Created for a confirmed payment; not on chain yet |
| `SUBMITTED` | Recorded on the settlement contract |
| `CONFIRMING` | Confirmation sent; the chain state is being checked |
| `CONFIRMED` | Verified on chain field by field and reconciled as `MATCHED` |
| `FAILED` | Rejected for good or out of attempts; the intent is `FAILED` |
## Reconciliation
Before anything is credited, Useroutr compares the chain with its own records. A settlement is
credited only when `reconciliation_status` is `MATCHED`. If they disagree, the status is
`EXCEPTION`, nothing is credited, and the case is opened for the Useroutr team to resolve.
## Read
```ts theme={null}
const settlement = await useroutr.fundingIntents.retrieveSettlement("fi_2d6a91c4");
settlement.status; // "CONFIRMED"
settlement.transfer_transaction_hash; // the transfer to your destination
settlement.protocol.confirm_transaction_hash; // the protocol confirmation
```
Every hash can be checked on a Stellar explorer, so you can verify a settlement yourself.
## Webhooks
`funding.settling` fires when a settlement is created, `funding.settled` when it is confirmed and
`funding.failed` if it fails. Each carries the funding intent, whose `settlement` field holds the
settlement.
Settlement is retried and recovers from partial failures on its own, without ever producing a
second credit.
# Refunds
Source: https://docs.useroutr.com/guides/refunds
How payments that cannot fund an intent go back to your users, and when you need to step in.
Every payment lands in the intent's own deposit account, so a payment that does not fund the
intent stays attributed to it and can always be sent back. Refunds are paid from that deposit
account by a Useroutr worker.
## Refunds that happen on their own
You do not need to do anything for these. Each fires `refund.created`, then `refund.completed`
or `refund.failed`.
| Reason | When |
| - | - |
| `OVERPAYMENT` | Your user sent more than the intent asked for. The excess goes back. |
| `WRONG_ASSET` | Your user paid in an asset the intent does not accept, for example XLM to a USDC intent |
| `EXPIRED_INTENT` | The payment's ledger closed after `expires_at` |
| `DUPLICATE_PAYMENT` | The intent was already fully funded when the payment arrived |
| `UNDERPAYMENT` | The intent expired partly funded. Every partial payment goes back. |
| `SETTLEMENT_FAILED` | The intent failed and was not recovered within 24 hours. What remains of each payment goes back. |
| `ROUTE_REMAINDER` | An exact-output quote used less of your user's asset than the maximum they sent |
| `ROUTE_SURPLUS` | An exact-input quote delivered more than your amount |
Each payment is refunded at most once, and never for more than it brought in or than its deposit
account still holds. A payment Useroutr cannot refund safely on its own, such as one whose funds
have already moved or an amount too small to be worth sending, is left for you to refund by
request.
## Where a refund goes, and when it waits for you
A refund goes back to the account the payment came from, but only when that is safe. If it is
not, the refund waits in `PENDING` with no `destination.address`, its `failure.code` says why, and
`refund.destination_required` fires when a refund that was about to pay out is stopped.
| `failure.code` | Why it waits |
| - | - |
| `NO_DESTINATION` | The payment arrived through a bridge, so there is no Stellar account to return it to |
| `DESTINATION_UNVERIFIED` | The payment carried a memo, so it most likely came from an exchange's shared account |
| `DESTINATION_REQUIRES_MEMO` | The account is a shared account that credits customers by memo |
| `DESTINATION_NOT_FOUND` | The account no longer exists on Stellar |
| `DESTINATION_NO_TRUSTLINE` | The account can no longer hold the refunded asset |
Sending an exchange's shared account a refund without the customer's memo would credit nobody, so
Useroutr never does. Ask your user where they want the refund, then give it a destination, with
the memo their exchange showed them if there is one:
```ts theme={null}
await useroutr.refunds.update("ref_6c2e9d41", {
destination_address: "GBVYOXQQZO2JYMG3UODZI5BSAXP3LKNNWJ7C5JCWUEGRZKXFG4NJITTL",
destination_memo: { type: "id", value: "3810274423" },
});
```
A memo is `text` (at most 28 bytes), `id` (an unsigned 64-bit integer in decimal) or `hash` (32
bytes in hex). The destination is checked when you set it: an account that does not exist, cannot
hold the asset, or requires a memo you did not give is refused with `INVALID_DESTINATION`.
## Payments made on Base, Arbitrum or Ethereum
A payment your user made to a deposit address on an EVM network is refunded on that network, in
the asset they sent, with the refund's `destination.network` set to that network. Useroutr
refunds on its own:
* what is left at the address after the payment went through, as `OVERPAYMENT` or
`ROUTE_REMAINDER`;
* a payment that could not be converted or delivered, once its deposit address allows it to be
taken back (seven days after the address was handed out), as `SETTLEMENT_FAILED`, `EXPIRED_INTENT`,
`UNDERPAYMENT` or `DUPLICATE_PAYMENT`. The payment is recorded as rejected, so the refund has a
payment to belong to.
The refund goes to the address that sent the tokens. A payment in the network's native currency,
or a deposit that more than one address paid, names no single sender, so the refund waits with
`NO_DESTINATION` for you to give it an address on that network (no memo):
```ts theme={null}
await useroutr.refunds.update("ref_91a0c7e3", {
destination_address: "0x4b20993Bc481177ec7E8f571ceCaE8A9e22C02db",
});
```
The refunded amount is what the deposit address actually held when it was taken back, which is
what the refund reports once it completes.
## Refund a payment yourself
Refund a rejected payment, or the confirmed payments of an intent that will not complete, by
naming the payment:
```ts theme={null}
const refund = await useroutr.refunds.create(
"fi_2d6a91c4",
"pay_4f7b0d12",
{ reason: "WRONG_ASSET" },
{ idempotencyKey: "refund-pay_4f7b0d12" },
);
```
The refund returns whatever of the payment has not been refunded already. Pass
`destination_address`, and `destination_memo` if it needs one, to send it somewhere other than
where it came from. One refund can be in progress per payment and asset at a time.
## Lifecycle
`PENDING` → `PROCESSING` → `COMPLETED` or `FAILED`. A completed refund carries the payout's
`transaction_hash`; a failed one carries its `failure`.
```ts theme={null}
const refund = await useroutr.refunds.retrieve("ref_6c2e9d41");
```
# Request logs
Source: https://docs.useroutr.com/guides/request-logs
See every call your server made with an API key, including the ones that failed.
Every request made with one of your API keys is logged, including requests rejected before they
did anything, such as a body that failed validation. When an integration misbehaves, the log
shows what actually reached Useroutr and what came back.
## In the dashboard
Open **Developers → Logs**. Filter to failed requests, or paste a request id from an error to
find the exact call. Each entry opens with its route, status, error code, duration, the key that
made it and its idempotency key.
## With the SDK
```ts theme={null}
const { data: failed } = await useroutr.requestLogs.list({ outcome: "failed" });
for (const log of failed) {
console.log(log.method, log.path, log.status, log.error_code, log.request_id);
}
```
Every error the SDK throws carries the request's id, so you can go straight to its log:
```ts theme={null}
import { UseroutrError } from "@useroutr/sdk";
try {
await useroutr.fundingIntents.create(params, { idempotencyKey: orderId });
} catch (error) {
if (error instanceof UseroutrError && error.requestId !== undefined) {
const { data } = await useroutr.requestLogs.list({ request_id: error.requestId });
console.log(data[0]);
}
throw error;
}
```
Filter by `outcome` (`succeeded` or `failed`), `request_id` or `api_key_id`.
## What is recorded
The method, the path without its query string, the matched route, the status, the error code, the
duration, the idempotency key, the key used and the request id.
Request and response bodies, headers and query strings are never recorded, so nothing your users
sent is kept in a log. Requests from the dashboard are not logged. Logs are kept for 30 days.
# Webhooks
Source: https://docs.useroutr.com/guides/webhooks
Get told the moment a payment is confirmed, settled or credited. Signed, retried and replayable.
Webhooks tell your server when your funding intents, payments and refunds change. Fulfil orders
on `funding.credited`: it arrives only after the payment is verified, settled and credited, and
it reaches you even when your user has closed the page.
## 1. Add an endpoint
In the dashboard, open **Developers → Webhooks → Add endpoint**, or create one with the SDK. The
signing secret is returned once, when the endpoint is created. Store it with your other secrets.
```ts theme={null}
const endpoint = await useroutr.webhooks.endpoints.create({
url: "https://example.com/webhooks/useroutr",
event_types: ["funding.credited", "funding.failed", "payment.rejected"],
});
endpoint.secret; // store as USEROUTR_WEBHOOK_SECRET
```
Pass `["*"]` to receive every event, including types added later. Endpoint URLs must be public
`https://` addresses.
## 2. Verify and handle events
`constructEvent` checks the signature over the **raw** request body and returns the typed event.
Never verify re-serialized JSON: the bytes must be exactly what was sent.
```ts Next.js (app router) theme={null}
import { Useroutr, WebhookSignatureError } from "@useroutr/sdk";
const useroutr = new Useroutr({ apiKey: process.env.USEROUTR_API_KEY });
export async function POST(request: Request) {
const payload = await request.text();
let event;
try {
event = useroutr.webhooks.constructEvent({
payload,
signatureHeader: request.headers.get("useroutr-signature") ?? undefined,
eventId: request.headers.get("useroutr-event-id") ?? undefined,
secret: process.env.USEROUTR_WEBHOOK_SECRET!,
});
} catch (error) {
if (error instanceof WebhookSignatureError) {
return new Response("invalid signature", { status: 400 });
}
throw error;
}
if (event.type === "funding.credited") {
await fulfilOrder(event.data.object.reference);
}
return new Response(null, { status: 200 });
}
```
```ts Express theme={null}
import express from "express";
import { Useroutr, WebhookSignatureError } from "@useroutr/sdk";
const app = express();
const useroutr = new Useroutr({ apiKey: process.env.USEROUTR_API_KEY });
app.post("/webhooks/useroutr", express.raw({ type: "application/json" }), (req, res) => {
let event;
try {
event = useroutr.webhooks.constructEvent({
payload: req.body,
signatureHeader: req.headers["useroutr-signature"],
eventId: req.headers["useroutr-event-id"],
secret: process.env.USEROUTR_WEBHOOK_SECRET!,
});
} catch (error) {
if (error instanceof WebhookSignatureError) {
res.status(400).end();
return;
}
throw error;
}
if (event.type === "funding.credited") {
void fulfilOrder(event.data.object.reference);
}
res.status(200).end();
});
```
Answer `2xx` quickly and do slow work afterwards. A request that takes longer than 10 seconds
counts as failed.
## 3. Make your handler safe to repeat
The same event can arrive more than once. Every delivery of an event carries the same `id` and
`Useroutr-Event-Id`, so record the ids you have handled and skip repeats. Treat a webhook as a
signal: when it matters, read the current state with the SDK.
## Events
| Event | When | `data.object` |
| - | - | - |
| `funding.created` | A funding intent is created | Funding intent |
| `funding.partially_funded` | A payment covered part of the amount | Funding intent |
| `funding.payment_confirmed` | Payments cover the full amount | Funding intent |
| `funding.settling` | A settlement was created | Funding intent |
| `funding.settled` | The settlement was confirmed on chain | Funding intent |
| `funding.credited` | Your ledger was credited | Funding intent |
| `funding.failed` | Registration or settlement failed | Funding intent |
| `funding.expired` | The intent expired unpaid | Funding intent |
| `payment.rejected` | A payment did not satisfy its intent | Payment |
| `refund.created` | A refund was recorded | Refund |
| `refund.completed` | A refund was paid out | Refund |
| `refund.failed` | A refund could not be paid | Refund |
| `refund.destination_required` | A refund is waiting for you to name the account to pay; see [Refunds](/guides/refunds) | Refund |
`data.object` is the resource as it is when the event is sent, which can already be further
along than the event's `type`. Use `type` for what happened and the object for the current state.
```json theme={null}
{
"id": "evt_0d5e8c26",
"object": "event",
"type": "funding.credited",
"api_version": "1",
"application_id": "app_5c1e0a9f",
"funding_intent_id": "fi_2d6a91c4",
"created_at": "2026-09-26T09:41:12.000Z",
"data": { "object": { "id": "fi_2d6a91c4", "object": "funding_intent", "status": "CREDITED" } }
}
```
## Retries
| Your response | Result |
| - | - |
| `2xx` | Delivered |
| `408`, `425`, `429`, `5xx`, a redirect, a timeout or a connection error | Retried |
| Any other `4xx` | Failed, not retried |
Retries back off exponentially, from 30 seconds up to an hour between attempts, for up to 8
attempts. Every attempt is recorded. Retry a failed delivery from the dashboard, or with the SDK:
```ts theme={null}
for await (const delivery of useroutr.webhooks.deliveries.iterate({ status: "EXHAUSTED" })) {
await useroutr.webhooks.deliveries.retry(delivery.id);
}
```
Every event is kept whether or not an endpoint received it, so you can replay what you missed
with `useroutr.webhooks.events.iterate()`.
## Signatures in detail
The SDK does this for you. To verify by hand, read `t` and every `v1` from the
`Useroutr-Signature` header (`t=,v1=`), then compute:
```text theme={null}
HMAC_SHA256(secret, `${t}.${eventId}.${rawBody}`) → hex
```
Accept if any `v1` matches under a constant-time comparison and `t` is within five minutes.
| Header | Value |
| - | - |
| `Useroutr-Signature` | `t=,v1=`, with two `v1` values during a secret rotation |
| `Useroutr-Event-Id` | The event id |
| `Useroutr-Event-Type` | The event type |
| `Useroutr-Delivery-Id` | The delivery id, stable across retries |
## Rotating the secret
```ts theme={null}
const { secret } = await useroutr.webhooks.endpoints.rotateSecret("whep_3b7a1f08");
```
For 24 hours both the old and the new secret sign every delivery, so you can deploy the new one
without missing an event. Pass both while you switch: `secret` accepts an array.
# How it works
Source: https://docs.useroutr.com/how-it-works
The path from a funding intent to a credit, and what Useroutr guarantees along it.
Every flow starts with a **funding intent**: your application's request to receive a specific
amount of a specific asset at a specific destination. Everything else exists to fulfil it.
Your server asks for, say, 25 USDC on Stellar, delivered to your account. Useroutr creates a
deposit account for this intent alone and registers the intent on its Soroban protocol.
If they hold the asset you asked for, they pay it directly. If they hold something else, a
[quote](/concepts/quotes-and-routes) prices the route: how much of their asset covers your
requirement, and how it gets converted.
The payment lands in the intent's deposit account, so no memo is needed. Useroutr verifies
the asset and amount on chain. A partial payment waits for the rest; an overpayment is
refunded.
The value moves to your destination through the Useroutr settlement contract. Useroutr then
checks the chain against its own records, field by field.
One credit is posted to your application's ledger, the intent becomes `CREDITED`, and a
signed `funding.credited` webhook reaches your server.
## What you can rely on
A funding intent produces exactly one credit. This is enforced by unique constraints in the
database, not only by application checks, and reconciliation flags any drift. Replayed
webhooks, retried jobs and repeated API calls cannot create a second credit.
A transaction hash is evidence, not completion. Useroutr credits only after the payment is
verified on chain and the settlement is confirmed and reconciled.
Useroutr holds no balances for you. Settled value goes to your own destination account; your
Useroutr balance is the accounting record of what has arrived there.
An asset is identified by its network and, on Stellar, its issuer, never by its symbol alone.
USDC from one issuer is never mistaken for another.
## Where the work happens
| Part | Where | What it does |
| - | - | - |
| Your server | The SDK or the API, with your API key | Creates funding intents, reads state, receives webhooks |
| Your user | Hosted page, embedded checkout or your own screens | Chooses an asset and pays |
| Useroutr | API, workers and the Soroban protocol | Quotes, detects, settles, reconciles, credits, notifies |
| Stellar | Testnet in the sandbox, mainnet in production | Carries the payment and the settlement |
# Welcome to Useroutr
Source: https://docs.useroutr.com/index
Let users fund your app with what they already hold. Your app receives exactly what it asked for, credited once.
Useroutr is application funding infrastructure. Your application says what it needs: an amount
of an asset, delivered to your account. Your user pays with whatever they have. Useroutr quotes
the route, watches for the payment, settles it on Stellar, reconciles it and credits your ledger,
then tells your server.
```ts theme={null}
import { Useroutr } from "@useroutr/sdk";
const useroutr = new Useroutr({ apiKey: process.env.USEROUTR_API_KEY });
const intent = await useroutr.fundingIntents.create(
{ amount: "25", asset: "asset_usdc_stellar" },
{ idempotencyKey: "ORDER-1042" },
);
// Send your user to intent.hosted_funding_url
```
Create a key, take a test payment and see it credited, in about ten minutes.
The path from a funding intent to a credit, and what Useroutr guarantees along it.
## Why Useroutr
Users pay with the asset they hold. A quote routes it into the asset you asked for, so you
never handle a conversion.
Nothing is credited until the payment is verified on chain and settled on the Useroutr
protocol.
A funding intent produces one credit, enforced by the database. Retries and replayed events
never pay you twice.
## Choose how you integrate
Redirect to a payment page Useroutr hosts. No frontend code.
Drop the React component into your app, with your logo and colour.
Build your own screens on quotes and funding instructions.
## Built for developers
Typed resources, retries, pagination and webhook verification.
A drop-in payment surface that follows the payment to completion.
Signed events for every step, delivered with retries.
Every endpoint, with SDK and cURL examples. Prefer HTTP? It is all there.
Useroutr is developer infrastructure. It is not a wallet, a bank or an exchange, and it holds no
balances for you: settled value goes to your own account, and your Useroutr ledger records it.
# Authentication
Source: https://docs.useroutr.com/integrate/authentication
API keys, their environments, and keeping them safe.
Your server authenticates with an API key that belongs to one of your applications. The SDK sends
it for you:
```ts theme={null}
const useroutr = new Useroutr({ apiKey: process.env.USEROUTR_API_KEY });
```
Over HTTP, send it as a bearer token:
```bash theme={null}
curl https://api.sandbox.useroutr.com/v1/applications/me \
-H "Authorization: Bearer $USEROUTR_API_KEY"
```
## Keys and environments
| Key | Environment | API |
| - | - | - |
| `ur_test_...` | Sandbox, Stellar testnet | `https://api.sandbox.useroutr.com` |
| `ur_live_...` | Production, Stellar mainnet | `https://api.useroutr.com` |
Each environment refuses the other's keys with `INVALID_CREDENTIALS` and a message naming the
mistake, so a test key can never move real funds.
## Creating and rotating keys
Create keys in the dashboard under **Developers → API keys**. A key's secret is shown once, when
it is created; Useroutr stores only a digest of it. Give each key a name, such as "Production
server", so you can tell them apart.
To rotate without downtime, create a new key, deploy it, then revoke the old one. Revoking takes
effect immediately.
```ts theme={null}
const key = await useroutr.applications.createApiKey(applicationId, {
idempotencyKey: "rotate-2026-09",
});
// deploy key.secret, then:
await useroutr.applications.revokeApiKey(applicationId, oldKeyId);
```
## Keep keys secret
An API key can create funding intents and read everything about your application. Keep it on your
server, in an environment variable or secret manager. Never put it in browser code, a mobile app
or a public repository.
Browsers never need a key: the [checkout](/payments/embedded-checkout) uses the funding token of
one intent, which grants nothing else.
## Scope
A key acts for exactly one application. Anything that belongs to another application answers
`404 NOT_FOUND`, never `403`, so ids cannot be probed. A suspended application answers
`403 APPLICATION_INACTIVE`.
## Rate limits
By default each key can make 600 requests a minute. Above that, requests answer `429 RATE_LIMITED` with a
`Retry-After` header and `x-ratelimit-limit`, `x-ratelimit-remaining` and `x-ratelimit-reset`.
The SDK waits and retries for you.
## Request ids
Every response carries an `X-Request-ID`, and every error repeats it as `requestId`. Send your own
`X-Request-ID` (up to 128 characters of letters, digits and `. _ : -`) to trace a call through your
own logs; the SDK accepts it as the `requestId` option.
# Errors
Source: https://docs.useroutr.com/integrate/errors
One error shape, a stable code for every failure, and what each one asks you to do.
Every failure uses the same envelope, and `code` never changes meaning. Branch on `code`, never
on `message`.
```json theme={null}
{
"error": {
"code": "INVALID_DESTINATION",
"message": "The destination asset must match the required asset.",
"requestId": "req_5f0c8a21",
"retryable": false,
"details": [{ "path": "/amount", "message": "must be a decimal string" }]
}
}
```
* `requestId` matches the `X-Request-ID` header. Quote it when you contact support, or find the
call in your [request logs](/guides/request-logs).
* `retryable` is `true` when the same request can succeed later.
* `details` lists each invalid field on a validation error.
Stack traces, database and chain errors are never returned.
## In the SDK
Every failure is a `UseroutrError`, and a subclass tells you the kind:
| Class | When |
| - | - |
| `ValidationError` | `400`: the request is invalid; see `details` |
| `AuthenticationError` | `401`: the key is missing, wrong, revoked or for the other environment |
| `AuthorizationError` | `403`: for example, the application is suspended |
| `NotFoundError` | `404`: not found, including anything owned by another application |
| `RateLimitError` | `429`: slow down; the SDK already waited and retried |
| `ApiError` | Any other API error: `409`, `422`, `5xx` |
| `ConnectionError` | The request never completed. `code` is `TIMEOUT` or `CONNECTION_ERROR`. |
| `WebhookSignatureError` | A webhook failed verification. Not a `UseroutrError`. |
```ts theme={null}
import { ApiError, UseroutrError } from "@useroutr/sdk";
try {
await useroutr.quotes.select(quoteId);
} catch (error) {
if (error instanceof ApiError && error.code === "QUOTE_EXPIRED") {
// quote again
} else if (error instanceof UseroutrError && error.retryable) {
// try again later
} else {
throw error;
}
}
```
## Codes
### Your request
| Code | HTTP | What to do |
| - | - | - |
| `VALIDATION_ERROR` | 400 | Fix the fields listed in `details` |
| `BAD_REQUEST` | 400, 413, 415 | The request is malformed, too large or of the wrong content type |
| `IDEMPOTENCY_KEY_REQUIRED` | 400 | Send an `Idempotency-Key` header |
| `IDEMPOTENCY_KEY_REUSED` | 409 | The key was used with a different body. Use a new key. |
| `IDEMPOTENCY_REQUEST_IN_PROGRESS` | 409 | The first request with this key is still running. Retry with the same key. |
| `CONFLICT` | 409 | The resource is in a state that conflicts, for example another quote is locked |
| `NOT_FOUND` | 404 | No such resource for your application |
### Access
| Code | HTTP | What to do |
| - | - | - |
| `UNAUTHORIZED` | 401 | Check the API key |
| `INVALID_CREDENTIALS` | 401 | The key is for the other environment: a test key on production or a live key on the sandbox |
| `FORBIDDEN` | 403 | The key cannot do this |
| `APPLICATION_INACTIVE` | 403 | The application is suspended or archived |
| `RATE_LIMITED` | 429 | Wait for `Retry-After`, then retry |
### Funding intents and quotes
| Code | HTTP | What to do |
| - | - | - |
| `INVALID_AMOUNT` | 422 | Use a positive decimal within the asset's precision |
| `UNSUPPORTED_ASSET` | 422 | Use an asset id from `assets.list()` |
| `INVALID_DESTINATION` | 422 | The destination cannot receive this asset; `message` says why |
| `INVALID_EXPIRATION` | 422 | Use an `expires_at` in the future and at most 24 hours ahead |
| `NO_ROUTE_AVAILABLE` | 422 | No route delivers this pair. Offer another asset. |
| `INSUFFICIENT_INPUT` | 422 | An exact-input amount buys less than you asked for |
| `QUOTE_EXPIRED` | 422 | Create a new quote |
| `QUOTE_NOT_SELECTABLE` | 422 | The intent cannot take a quote now |
| `QUOTE_NOT_LOCKED` | 422 | The quote is not the intent's locked quote |
| `PROTOCOL_REJECTED` | 422 | The protocol refused the intent |
| `PROTOCOL_UNAVAILABLE` | 503 | Retryable. The protocol did not answer in time. |
| `ROUTE_UNAVAILABLE` | 503 | Retryable. No provider answered in time. |
### Branding
| Code | HTTP | What to do |
| - | - | - |
| `LOGO_FORMAT_UNSUPPORTED` | 422 | Upload a PNG, JPEG or WebP |
| `LOGO_DIMENSIONS_INVALID` | 422 | Use an image 64 to 1024 pixels on each side |
| `LOGO_STORAGE_UNAVAILABLE` | 503 | Retryable. Logo storage is not available. |
### Anything else
| Code | HTTP | What to do |
| - | - | - |
| `INTERNAL_ERROR` | 500 | Something failed on our side. Before repeating a write without an idempotency key, check whether it took effect. |
# Idempotency
Source: https://docs.useroutr.com/integrate/idempotency
Retry any money-moving request safely: it happens once, and repeats return the same result.
Networks fail. A request can succeed on Useroutr while your server never hears back. An
idempotency key makes that safe: send the same request again with the same key and you get the
original result, not a second funding intent.
```ts theme={null}
const intent = await useroutr.fundingIntents.create(
{ amount: "25", asset: "asset_usdc_stellar", reference: order.id },
{ idempotencyKey: order.id },
);
```
Use a key that names the business action, like your order id, not a random value per attempt.
Then a retry after a crash replays instead of duplicating.
## How it works
* The first request with a key runs, and its response is kept for 24 hours.
* A repeat with the same key and the same body returns that response, with the header
`Idempotency-Replayed: true`.
* The same key with a different body is refused with `409 IDEMPOTENCY_KEY_REUSED`.
* A repeat that arrives while the first is still running gets
`409 IDEMPOTENCY_REQUEST_IN_PROGRESS`, which is retryable.
* If the first request fails, the key is released so you can retry with it.
Keys are up to 255 characters and scoped to your application.
## Where it applies
| Request | Key |
| - | - |
| Create a funding intent | **Required** |
| Create a refund | Optional |
| Create a webhook endpoint, rotate its secret | Optional |
| Create an API key | Optional |
| Update requests (`PATCH`) | Not needed: they set absolute values |
| Revoke a key, retry a delivery | Not needed: repeating changes nothing |
## In the SDK
The SDK retries reads, and writes that carry an idempotency key, on timeouts, connection errors,
`429` and server errors. A write without a key is never retried, so it can never be applied
twice.
```ts theme={null}
const { data, idempotencyReplayed } = await useroutr.request({
method: "POST",
path: "/v1/funding-intents",
body: { amount: "25", asset: "asset_usdc_stellar" },
idempotencyKey: "ORDER-1042",
});
```
# Pagination
Source: https://docs.useroutr.com/integrate/pagination
Walk long lists with a cursor, or let the SDK do it for you.
Lists are returned newest first, one page at a time, with a cursor to the next page.
```json theme={null}
{
"object": "list",
"data": [{ "id": "fi_2d6a91c4" }, { "id": "fi_7c01e5b8" }],
"next_cursor": "fi_7c01e5b8"
}
```
## With the SDK
`iterate` fetches pages as you go, so you can loop over everything:
```ts theme={null}
for await (const intent of useroutr.fundingIntents.iterate({ status: "CREDITED" })) {
console.log(intent.id);
}
```
`list` returns a single page when you want to control paging yourself:
```ts theme={null}
const page = await useroutr.fundingIntents.list({ limit: 50 });
const next = page.next_cursor
? await useroutr.fundingIntents.list({ limit: 50, cursor: page.next_cursor })
: null;
```
## Over HTTP
Pass `limit` (1 to 100; 50 by default, 20 for quotes) and, after the first page, `cursor` set to
the previous page's `next_cursor`. When `next_cursor` is `null`, you have everything.
```bash theme={null}
curl "https://api.sandbox.useroutr.com/v1/funding-intents?limit=50&cursor=fi_7c01e5b8" \
-H "Authorization: Bearer $USEROUTR_API_KEY"
```
# Sandbox
Source: https://docs.useroutr.com/integrate/sandbox
Build and test against the Stellar testnet, then go live by changing one key.
The sandbox is a complete Useroutr environment on the Stellar testnet. Everything works the same
as production: funding intents, quotes, settlement, the ledger, webhooks and the checkout. No
real money moves.
| | Sandbox | Production |
| - | - | - |
| API key | `ur_test_...` | `ur_live_...` |
| API | `https://api.sandbox.useroutr.com` | `https://api.useroutr.com` |
| Network | Stellar testnet | Stellar mainnet |
The SDK reads the key's prefix and calls the right API, so your code does not change.
## Paying in the sandbox
Payments are real testnet transactions. To pay a funding intent:
1. Create a testnet account in [Stellar Lab](https://lab.stellar.org) and fund it with Friendbot,
which gives you free testnet XLM.
2. Open the intent's `hosted_funding_url`, or send the amount in `funding_instructions` to its
deposit address from that account.
To pay in testnet USDC, add a trustline for the testnet USDC issuer to your account first and get
testnet USDC from Circle's testnet faucet. The assets available in the sandbox are listed by
`useroutr.assets.list()`.
Your destination account must exist on testnet too, and hold a trustline for any issued asset you
ask to receive.
## Going live
1. Create a `ur_live_` key in the dashboard.
2. Set a mainnet destination account in **Settings**.
3. Point your webhook endpoints at production and store their new secrets.
4. Swap the key in your server's environment.
Test keys, test data and test webhooks stay in the sandbox. Nothing crosses between the two.
# Branding
Source: https://docs.useroutr.com/payments/branding
Your logo and colour on every checkout, hosted or embedded, with no code.
Set your brand once and every checkout for your application wears it: the hosted page and the
embedded component alike.
## In the dashboard
Open **Settings → Branding**. Upload a logo, pick a colour from the presets or enter any hex
value, and watch the live preview update before you save. Labels on your colour switch between
white and dark automatically so they stay readable.
## With the SDK
```ts theme={null}
import { readFile } from "node:fs/promises";
await useroutr.applications.update(applicationId, { brand_color: "#1d4ed8" });
const application = await useroutr.applications.setLogo(
applicationId,
await readFile("logo.png"),
"image/png",
);
application.branding; // { brand_color: "#1d4ed8", logo_url: "https://..." }
```
`brand_color` accepts `#rgb` or `#rrggbb` and is returned as lower-case `#rrggbb`. Send `null`
to go back to the Useroutr default. Remove a logo with `useroutr.applications.removeLogo(id)`.
### Logo rules
| Rule | Limit |
| - | - |
| Format | PNG, JPEG or WebP, read from the file itself. SVG is not accepted. |
| Size | Up to 256 KB |
| Dimensions | 64 to 1024 pixels on each side. Square reads best. |
A renamed file is refused with `LOGO_FORMAT_UNSUPPORTED`, and one outside the size limits with
`LOGO_DIMENSIONS_INVALID`. Uploading the logo you already have changes nothing, and a new logo
gets a new URL, so an old cached logo is never shown in its place.
## For one embedded checkout
`appearance` on the [React checkout](/payments/embedded-checkout) overrides your saved branding
for that checkout only. Each option replaces just its own part, so passing only `radius` keeps
your colour and logo.
```tsx theme={null}
```
| Option | Accepts | Changes |
| - | - | - |
| `brandColor` | `#rgb` or `#rrggbb` | Buttons, selection, focus, links and progress |
| `logoUrl` | an `https:` or `data:image/` URL | The mark in the header |
| `radius` | `"sharp"`, `"default"` or `"round"` | Corners of the panel, cards, rows and buttons |
| `fontFamily` | a font family name | Text. Load the font on your page; the checkout never downloads one |
A value the checkout cannot use is ignored, so a mistake keeps the default instead of breaking
the payment screen.
Success, failure and warning colours never follow your brand. A user should never read a pending
or failed payment as complete because your colour happens to be green.
# Custom checkout
Source: https://docs.useroutr.com/payments/custom-checkout
Build your own payment screens on quotes and funding instructions.
When the hosted page and the component do not fit, build the screens yourself. Useroutr still
does the hard part: it prices the route, creates the deposit account, detects the payment and
settles it. You decide how each step looks.
## The flow
```ts theme={null}
const intent = await useroutr.fundingIntents.create(
{ amount: "25", asset: "asset_usdc_stellar", reference: orderId },
{ idempotencyKey: orderId },
);
```
Offer the assets Useroutr can take. `network` and `symbol` are for display; the `id` is what
you send back.
```ts theme={null}
const { data: assets } = await useroutr.assets.list();
```
If your user holds the asset you asked for, skip to step 4: no quote is needed.
A quote says how much of your user's asset covers your requirement. Selecting it locks the
price and switches the intent's instructions to that asset.
```ts theme={null}
const quote = await useroutr.quotes.create(intent.id, {
source: { network: "stellar", asset: "asset_xlm_stellar" },
});
// quote.source.amount is the most your user sends; quote.fees lists every fee
await useroutr.quotes.select(quote.id);
```
See [Quotes and routes](/concepts/quotes-and-routes) for exact-input quotes, slippage and
routes across networks.
Read the intent again and show exactly what `funding_instructions` says.
```ts theme={null}
const { funding_instructions: instructions } = await useroutr.fundingIntents.retrieve(intent.id);
// instructions.deposit.address where to send, created for this intent alone
// instructions.amount what is still owed, in instructions.asset
// instructions.expires_at when the intent stops taking payment
```
No memo is needed: the deposit address alone identifies the intent.
Listen for [webhooks](/guides/webhooks) on your server, or poll the intent. Show progress
from its `status`: `PARTIALLY_FUNDED` means send the rest, `PAYMENT_CONFIRMED` onward means
received, `CREDITED` means done.
## Building it in the browser
Your browser code can read an intent's state without an API key through the public checkout
endpoints, addressed by the funding token. `@useroutr/checkout-core` wraps them together with
the checkout's state machine, so you get the same states the embedded checkout uses and render
them your way.
```ts theme={null}
import { CheckoutController, HttpCheckoutApi } from "@useroutr/checkout-core";
const controller = new CheckoutController({
api: new HttpCheckoutApi({ baseUrl: "https://api.sandbox.useroutr.com/v1/public", token }),
});
controller.subscribe(() => {
const state = controller.getState();
render(state.status, state.intent); // your own rendering
});
await controller.start(); // loads the intent and keeps polling it
```
The controller also takes your user's choices: `selectMethod`, `selectNetwork` and
`selectAsset`, plus `back` and `retry`. Call `stop` when the screen goes away.
Creating the intent, quoting for amounts you decide and reading your ledger stay on your server,
with your API key.
# Embedded checkout
Source: https://docs.useroutr.com/payments/embedded-checkout
Take payment inside your app with the React checkout, styled with your brand.
`@useroutr/checkout-react` is the same checkout as the hosted page, as a React component. It
opens as a modal or renders inline, shows your user what to send and follows the payment to
completion. Your API key never touches the browser: the component needs only the intent's
funding token.
```bash npm theme={null}
npm install @useroutr/sdk @useroutr/checkout-react
```
```bash pnpm theme={null}
pnpm add @useroutr/sdk @useroutr/checkout-react
```
```bash yarn theme={null}
yarn add @useroutr/sdk @useroutr/checkout-react
```
The amount is yours to decide, so decide it where your user cannot change it. Return only the
funding token, the last segment of `hosted_funding_url`.
```ts app/actions.ts theme={null}
"use server";
import { Useroutr } from "@useroutr/sdk";
const useroutr = new Useroutr({ apiKey: process.env.USEROUTR_API_KEY });
export async function startCheckout(orderId: string) {
const intent = await useroutr.fundingIntents.create(
{ amount: "25", asset: "asset_usdc_stellar", reference: orderId },
{ idempotencyKey: orderId },
);
const url = intent.hosted_funding_url ?? "";
return url.slice(url.lastIndexOf("/") + 1);
}
```
```tsx app/pay-button.tsx theme={null}
"use client";
import { UseroutrCheckout } from "@useroutr/checkout-react";
import { useState } from "react";
import { startCheckout } from "./actions";
export function PayButton({ orderId }: { orderId: string }) {
const [token, setToken] = useState(null);
return (
<>
{token === null ? null : (
console.log("paid")}
onClose={() => setToken(null)}
/>
)}
>
);
}
```
Use `https://api.useroutr.com/v1/public` in production.
`onSuccess` is for your interface: close the modal, show a thank-you. Fulfil the order on
your server when the [`funding.credited` webhook](/guides/webhooks) arrives. It is signed, and
it reaches you even if your user closes the page before the payment completes.
## Props
The funding token from the intent's `hosted_funding_url`. It grants exactly one intent.
Where the checkout's three public endpoints live: `https://api.sandbox.useroutr.com/v1/public`
or `https://api.useroutr.com/v1/public`. They answer any origin, so you do not need to proxy
them.
Open over your page, or render in place.
The colour scheme.
Override your saved branding for this checkout: `brandColor`, `logoUrl`, `radius` and
`fontFamily`. See [Branding](/payments/branding).
Called once, when the funding intent is credited, with the payer's view of the intent.
Called once, when the checkout cannot complete: the intent failed, expired or cannot be loaded.
Called when your user closes the modal.
## The token is the credential
The endpoints the checkout calls take no API key, cookie or session. They are addressed by the
funding token, which is unguessable and grants one intent. Treat it like a payment link: give
it to the person paying and nobody else.
If you find yourself putting an API key in the browser, the funding intent is being created in
the wrong place. Move it to your server.
# Hosted checkout
Source: https://docs.useroutr.com/payments/hosted-checkout
Send your user to a payment page Useroutr hosts. No frontend code.
Every funding intent comes with `hosted_funding_url`, a payment page for that intent alone. It
shows your logo and colour, lets the user pick how to pay, shows exactly what to send and
follows the payment through to completion.
## Create the intent and redirect
Create the funding intent on your server, then redirect your user to its page.
```ts app/checkout/route.ts theme={null}
import { Useroutr } from "@useroutr/sdk";
import { redirect } from "next/navigation";
const useroutr = new Useroutr({ apiKey: process.env.USEROUTR_API_KEY });
export async function POST(request: Request) {
const { orderId } = await request.json();
const intent = await useroutr.fundingIntents.create(
{ amount: "25", asset: "asset_usdc_stellar", reference: orderId },
{ idempotencyKey: orderId },
);
redirect(intent.hosted_funding_url!);
}
```
Use your own order id as the idempotency key. If the request is retried, you get the same
intent back instead of a second one.
## Fulfil on the webhook
The page tells your user when their payment is confirmed, but your server should fulfil the
order when it receives `funding.credited`. That event arrives only once the payment is settled
and your ledger is credited, and it arrives even if your user closes the tab.
```ts theme={null}
if (event.type === "funding.credited") {
await fulfilOrder(event.data.object.reference);
}
```
See [Webhooks](/guides/webhooks) for verifying and handling events.
## What the page handles
* The ways your user can pay, with the amount due in the asset they pick.
* A deposit address and exact amount, with copy buttons and a QR code.
* Partial payments, which it keeps asking to complete.
* Expiry: once `expires_at` passes, the page stops taking payment.
* Your branding. Set your logo and colour once in the dashboard; see [Branding](/payments/branding).
The link grants access to one funding intent and nothing else, so it is safe to email or put in
a QR code. Give it only to the person paying.
# Quickstart
Source: https://docs.useroutr.com/quickstart
Take a test payment and watch it get credited, from a fresh account, in about ten minutes.
You will create an API key, ask for 10 XLM, pay it on the Stellar testnet and see your
application credited. Everything here runs in the sandbox, so no real money moves.
Sign up at [dashboard.useroutr.com](https://dashboard.useroutr.com), verify your email and
create your first application. An application is the product or service that receives the
funding.
In the dashboard, open **Developers → API keys** and create a key. It starts with `ur_test_`,
which points the SDK at the sandbox. The secret is shown once, so store it now.
```bash .env theme={null}
USEROUTR_API_KEY=ur_test_...
```
An API key belongs on your server. Never ship it to a browser or a mobile app.
```bash npm theme={null}
npm install @useroutr/sdk
```
```bash pnpm theme={null}
pnpm add @useroutr/sdk
```
```bash yarn theme={null}
yarn add @useroutr/sdk
```
```bash bun theme={null}
bun add @useroutr/sdk
```
The SDK needs Node.js 20 or later.
Settled value goes to a Stellar account you control. For this test, create a testnet account
in [Stellar Lab](https://lab.stellar.org) and fund it with Friendbot, then set its address as
the default destination in **Settings → Default destination**. You can also pass a
`destination` on each funding intent instead.
A funding intent asks for an exact amount of an exact asset. The idempotency key makes the
call safe to retry: send the same key again and you get the same intent back.
```ts create-intent.ts theme={null}
import { Useroutr } from "@useroutr/sdk";
const useroutr = new Useroutr({ apiKey: process.env.USEROUTR_API_KEY });
const intent = await useroutr.fundingIntents.create(
{ amount: "10", asset: "asset_xlm_stellar", reference: "quickstart-1" },
{ idempotencyKey: "quickstart-1" },
);
console.log(intent.id, intent.status);
console.log(intent.hosted_funding_url);
```
The intent starts `CREATED` and becomes `AWAITING_PAYMENT` within seconds, once it is
registered on the Useroutr protocol.
Open `hosted_funding_url` and follow the checkout, or pay directly: the intent's
`funding_instructions` name a deposit account created for this intent alone and the exact
amount. Send 10 testnet XLM to `funding_instructions.deposit.address` from any testnet wallet.
No memo is needed.
Useroutr detects the payment, verifies it, settles it and posts one credit to your ledger.
Poll the intent until it reaches `CREDITED`, or better, [receive a webhook](/guides/webhooks).
```ts theme={null}
const current = await useroutr.fundingIntents.retrieve(intent.id);
console.log(current.status); // "CREDITED"
```
The payment, the settlement and the credit are all visible in the dashboard too.
## What you just proved
The whole path works: a request for value, a payment on chain, a verified settlement and exactly
one credit. Everything else builds on this.
Take the payment inside your app with the React component.
Fulfil orders the moment a funding intent is credited.
# SDKs
Source: https://docs.useroutr.com/sdks/overview
Official libraries for your server and your frontend.
The SDKs are the recommended way to integrate. They are typed end to end, retry what is safe to
retry, paginate for you and verify webhooks. Everything they do is also available over
[HTTP](/api-reference/introduction) if you prefer.
`@useroutr/sdk` for your server: funding intents, quotes, settlements, the ledger, refunds,
webhooks and request logs.
`@useroutr/checkout-react` for your frontend: a drop-in checkout that follows the payment to
completion.
## Which one do I need?
| You want to | Use |
| - | - |
| Create funding intents, read state, handle webhooks | `@useroutr/sdk` on your server |
| Show a checkout inside your React app | `@useroutr/checkout-react` in the browser |
| Build a checkout for another framework | `@useroutr/checkout-core` in the browser |
| Share Useroutr types across your code | `@useroutr/types`, installed with the SDK |
A typical integration uses two: the Node.js SDK creates the funding intent on your server, and
the React checkout takes the payment in the browser with the intent's funding token. Your API
key stays on the server.
Using another language? Every endpoint is plain JSON over HTTPS, with a bearer API key. The
[API reference](/api-reference/introduction) has a cURL example beside each one.
# React checkout
Source: https://docs.useroutr.com/sdks/react
A drop-in checkout for React 18 and 19 that follows the payment to completion.
`@useroutr/checkout-react` is the Useroutr checkout as a React component. It shows your user what
to send, where and on which network, follows the payment to completion, and wears your logo and
colour.
## Install
```bash npm theme={null}
npm install @useroutr/checkout-react
```
```bash pnpm theme={null}
pnpm add @useroutr/checkout-react
```
```bash yarn theme={null}
yarn add @useroutr/checkout-react
```
React 18 or 19 is required. The component brings its own styles and never downloads a font.
## Use it
```tsx theme={null}
import { UseroutrCheckout } from "@useroutr/checkout-react";
showThankYou(intent)}
/>;
```
The `token` comes from a funding intent your server created with the
[Node.js SDK](/sdks/typescript). The [Embedded checkout](/payments/embedded-checkout) guide walks
through the whole flow, and lists every prop.
## Brand it
Your saved logo and colour apply automatically. Override them for one checkout with
`appearance`:
```tsx theme={null}
```
See [Branding](/payments/branding).
## Logos
The package exports the brand marks it uses, so your own screens can match the checkout:
```tsx theme={null}
import { assetMark, networkMark } from "@useroutr/checkout-react";
const Usdc = assetMark("USDC"); // a component, or undefined when there is no mark
const Stellar = networkMark("stellar");
```
## Other frameworks
The checkout's state machine and API client live in the framework-free
`@useroutr/checkout-core`. Use it to build the same checkout in Vue, Svelte or plain
TypeScript. See [Custom checkout](/payments/custom-checkout#building-it-in-the-browser).
# Node.js SDK
Source: https://docs.useroutr.com/sdks/typescript
The typed client for the Useroutr API, for Node.js 20 and later.
`@useroutr/sdk` is the official client for your server. It is written in TypeScript, ships its
own types, and has no dependencies beyond Useroutr's type package.
## Install
```bash npm theme={null}
npm install @useroutr/sdk
```
```bash pnpm theme={null}
pnpm add @useroutr/sdk
```
```bash yarn theme={null}
yarn add @useroutr/sdk
```
```bash bun theme={null}
bun add @useroutr/sdk
```
## Quickstart
```ts theme={null}
import { Useroutr } from "@useroutr/sdk";
const useroutr = new Useroutr({ apiKey: process.env.USEROUTR_API_KEY });
const intent = await useroutr.fundingIntents.create(
{ amount: "25", asset: "asset_usdc_stellar", reference: "ORDER-1042" },
{ idempotencyKey: "ORDER-1042" },
);
```
## Sandbox and production
The key picks the environment, so going live is a change of key and nothing else.
| Key | Calls |
| - | - |
| `ur_test_...` | The sandbox, `https://api.sandbox.useroutr.com`, on the Stellar testnet |
| `ur_live_...` | Production, `https://api.useroutr.com`, on Stellar mainnet |
Each environment refuses the other's keys with a message that says so.
## Options
```ts theme={null}
const useroutr = new Useroutr({
apiKey: process.env.USEROUTR_API_KEY,
timeoutMs: 30_000, // per request
maxRetries: 2, // reads and writes with an idempotency key
baseUrl: "http://localhost:4000", // only to point somewhere else
});
```
`baseUrl` overrides the key's environment, and so does the `USEROUTR_BASE_URL` environment
variable when `baseUrl` is not given.
## Resources
| Resource | Methods |
| - | - |
| `fundingIntents` | `create`, `retrieve`, `list`, `iterate`, `listPayments`, `retrieveSettlement`, `retrievePublic` |
| `quotes` | `create`, `retrieve`, `list`, `iterate`, `select` |
| `assets` | `list` |
| `payments` | `list`, `iterate`, `listForFundingIntent` |
| `settlements` | `retrieve`, `list`, `iterate`, `retrieveForFundingIntent` |
| `balances` | `list` |
| `ledger` | `list`, `iterate` |
| `refunds` | `create`, `retrieve`, `update` |
| `webhooks.endpoints` | `create`, `retrieve`, `list`, `iterate`, `update`, `delete`, `rotateSecret` |
| `webhooks.events` | `retrieve`, `list`, `iterate` |
| `webhooks.deliveries` | `retrieve`, `list`, `iterate`, `retry` |
| `webhooks` | `constructEvent`, `verifySignature` |
| `requestLogs` | `list`, `iterate`, `retrieve` |
| `applications` | `current`, `retrieve`, `update`, `overview`, `setLogo`, `removeLogo`, `listApiKeys`, `createApiKey`, `revokeApiKey` |
Every method takes a last, optional argument with `idempotencyKey`, `timeoutMs` and `requestId`.
Response types come from `@useroutr/types` and are re-exported by the SDK:
```ts theme={null}
import { type FundingIntentResponse, type WebhookEvent } from "@useroutr/sdk";
```
## Pagination
`list` returns one page. `iterate` walks every page for you:
```ts theme={null}
for await (const intent of useroutr.fundingIntents.iterate({ status: "CREDITED" })) {
console.log(intent.reference);
}
```
See [Pagination](/integrate/pagination).
## Errors
Every failure is a `UseroutrError` with `code`, `statusCode`, `requestId`, `retryable` and
`details`, and a subclass tells you the kind:
```ts theme={null}
import { UseroutrError, ValidationError } from "@useroutr/sdk";
try {
await useroutr.fundingIntents.create(params, { idempotencyKey: orderId });
} catch (error) {
if (error instanceof ValidationError) {
console.error(error.details);
} else if (error instanceof UseroutrError && error.retryable) {
// safe to try again later with the same idempotency key
} else {
throw error;
}
}
```
See [Errors](/integrate/errors) for every class and code.
## Retries and timeouts
Reads, and writes that carry an idempotency key, are retried on `408`, `425`, `429`, `500`,
`502`, `503` and `504`, on timeouts and on connection errors, up to `maxRetries` times, honouring
`Retry-After`. Writes without a key are
never retried, so a network error can never create a duplicate. A timeout surfaces as a
`ConnectionError` with `code: "TIMEOUT"`.
## Webhooks
`constructEvent` verifies the signature and returns the typed event. See
[Webhooks](/guides/webhooks) for complete handlers.
```ts theme={null}
const event = useroutr.webhooks.constructEvent({
payload: rawBody,
signatureHeader: headers["useroutr-signature"],
eventId: headers["useroutr-event-id"],
secret: process.env.USEROUTR_WEBHOOK_SECRET!,
});
```
## Anything else
`request` calls any endpoint directly and also returns the request id and whether an idempotent
call was replayed:
```ts theme={null}
const { data, requestId, idempotencyReplayed } = await useroutr.request({
method: "GET",
path: "/v1/funding-intents",
});
```