# 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. The hosted checkout, showing the amount due and the ways to pay ## 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", }); ```