Skip to Content
PaymentsIntegrate

Integrate the Payments API

A complete walk-through from zero to your first received payment. By the end of this guide you’ll have an API key, an environment, a wallet, and a paid invoice in either BTC or a stablecoin.

This guide covers the API integration path. Every step shown here can also be done through the dashboard UI at rails.amboss.tech/pay . Use the dashboard if you prefer a UI-driven setup; use the API for everything you want to automate from your backend.

The API is a single GraphQL endpoint at https://rails.amboss.tech/graphql. Everything below uses that endpoint. You can open it in Apollo Studio here  for interactive exploration.

What you’ll build

Prerequisites

  • An Amboss account with dashboard access at account.amboss.tech . The first account on a team becomes the master account, which is the only role that can create or revoke API keys.
  • A publicly reachable HTTPS URL to receive webhooks (we recommend smee.io  or ngrok  for local development).
  • Your stack of choice. The API is standard GraphQL, callable from any language.

Start in a sandbox environment first. Sandbox payments behave identically to live but don’t require any Lightning node setup or on-chain funds.


Quickstart

Create an environment

An environment is a logical container that holds wallets, API keys, and webhook endpoints. You can have many; typical setups have one SANDBOX for development and one LIVE for production.

Environment creation is a master-account operation, so it uses dashboard auth rather than an API key. Grab the auth token value from your logged-in session cookie and pass it as a Bearer token in the Authorization header.

create-environment.gql
mutation CreateEnvironment { payment { environment { create(input: { name: "Sandbox - dev" type: SANDBOX }) { id name type created_at } } } }
response.json
{ "data": { "payment": { "environment": { "create": { "id": "81b73615-ddf3-46e3-943a-467c3e442e04", "name": "Sandbox - dev", "type": "SANDBOX", "created_at": "2026-06-02T12:30:00.000Z" } } } } }

Save the returned id. You’ll use it for every subsequent call. Inside the dashboard you can find it in the URL of the environment page (/pay/<environment_id>).

Create an API key

API keys let your backend authenticate without holding a user session. Keys are scoped to a resource (environments, wallets, payments, webhooks) and a level (READ or WRITE). Grant only what your integration needs.

API key creation requires master-account dashboard auth (you cannot create one API key with another).

create-api-key.gql
mutation CreateApiKey { service_api_key { create(input: { name: "backend-prod" environment_id: "81b73615-ddf3-46e3-943a-467c3e442e04" permissions: [ { resource: WALLETS, level: WRITE } { resource: PAYMENTS, level: WRITE } { resource: WEBHOOKS, level: WRITE } ] }) { plaintext_key key { id name key_prefix key_suffix permissions { resource level } } } } }
response.json
{ "data": { "service_api_key": { "create": { "plaintext_key": "amb_test_PvDaoBz6TXhuMKxk0VkckiPUTX9BTSDyCT2Pqc_vgOw", "key": { "id": "28826356-6213-4aad-ae56-8c86e7cf8923", "name": "backend-prod", "key_prefix": "amb_test_PvDa", "key_suffix": "vgOw", "permissions": [ { "resource": "WALLETS", "level": "WRITE" }, { "resource": "PAYMENTS", "level": "WRITE" }, { "resource": "WEBHOOKS", "level": "WRITE" } ] } } } } }

plaintext_key is returned once at creation and is never retrievable afterwards. Store it in your secret manager immediately. If you lose it, revoke and create a new one.

From here on, every API call uses the key via the x-api-key header. No dashboard auth needed.

Create a wallet

A wallet holds balance in exactly one asset (BTC sats or a stablecoin like USDT/USDC). You’ll need its asset_id, which you can list with the taproot_assets query (for stablecoins) or use the BTC base-asset id.

list-assets.gql
query AvailableAssets { taproot_assets { list { id symbol precision } } }
create-wallet.gql
mutation CreateWallet { payment { wallet { create(input: { name: "Main USDT wallet" environment_id: "81b73615-ddf3-46e3-943a-467c3e442e04" asset_id: "<asset_id_from_above>" }) { id name asset { symbol precision } balance { balance received sent } } } } }
response.json
{ "data": { "payment": { "wallet": { "create": { "id": "5e4b1e2a-9f3c-4a5b-8c7d-1234567890ab", "name": "Main USDT wallet", "asset": { "symbol": "USDT", "precision": 6 }, "balance": { "balance": "0", "received": "0", "sent": "0" } } } } } }

Amounts everywhere in the API are decimal strings in the asset’s minor units. For BTC that’s satoshis (precision 8, so 100000 = 0.001 BTC). For USDT/USDC, that’s the asset’s smallest unit per its precision field.

Create a receive invoice

Generate a Lightning invoice the payer can pay. Returns a BOLT11 string and a transaction record you can track.

create-receive.gql
mutation CreateReceive { payment { transaction { create_receive(input: { wallet_id: "5e4b1e2a-9f3c-4a5b-8c7d-1234567890ab" amount: "100000" description: "Order #42" expires_in_seconds: 3600 idempotency_key: "order-42-attempt-1" metadata: "{\"order_id\":\"42\"}" }) { id status payment_request payment_hash expires_at amount { display_amount full_amount } } } } }
response.json
{ "data": { "payment": { "transaction": { "create_receive": { "id": "tx_01HX9YQK7P8MVZ3FN4G2RWS6CD", "status": "PENDING", "payment_request": "lnbc1m1p0...", "payment_hash": "3b6e7d...", "expires_at": "2026-06-02T13:30:00.000Z", "amount": { "display_amount": "0.001", "full_amount": "100000" } } } } } }

Render payment_request as a QR code (or a lightning: link) and present it to the payer. Once they pay, the transaction status moves from PENDINGCOMPLETED.

Idempotency. idempotency_key is optional but recommended. If the same key is replayed within the wallet, the API returns the original invoice instead of creating a duplicate, which makes it safe to retry from your side.

Listen for payments via webhooks

You could poll the transaction by id, but webhooks are the production path. Register your endpoint once; we deliver every relevant payment event.

create-webhook.gql
mutation CreateWebhookEndpoint { payment { webhook_endpoint { create(input: { environment_id: "81b73615-ddf3-46e3-943a-467c3e442e04" url: "https://api.example.com/webhooks/amboss" event_filters: ["payment.completed", "payment.failed", "payment.expired"] }) { secret endpoint { id url event_filters active } } } } }
response.json
{ "data": { "payment": { "webhook_endpoint": { "create": { "secret": "whsec_...", "endpoint": { "id": "wep_01HX9YQK7P8MVZ3FN4G2RWS6CD", "url": "https://api.example.com/webhooks/amboss", "event_filters": ["payment.completed", "payment.failed", "payment.expired"], "active": true } } } } } }

The secret is returned once at creation. Use it to verify the signature header on every delivery (see Verifying webhooks below). If lost, rotate it with the rotate_secret mutation.

Example incoming payload for payment.completed:

webhook-payload.json
{ "id": "payment.completed:tx_01HX9YQK7P8MVZ3FN4G2RWS6CD", "event_type": "payment.completed", "environment": "sandbox", "environment_id": "81b73615-ddf3-46e3-943a-467c3e442e04", "wallet_id": "5e4b1e2a-9f3c-4a5b-8c7d-1234567890ab", "node_id": null, "data": { "id": "tx_01HX9YQK7P8MVZ3FN4G2RWS6CD", "direction": "receive", "status": "completed", "payment_details": { "payment_type": "bolt11", "payment_request": "lnbc1m1p0...", "payment_hash": "3b6e7d..." }, "amount": { "amount": "100000", "asset_id": "...", "asset_symbol": "USDT", "precision": 6 }, "fee": { "amount": "5", "asset_id": "...", "asset_symbol": "USDT", "precision": 6 }, "settle_amount": { "amount": "99995", "asset_id": "...", "asset_symbol": "USDT", "precision": 6 }, "exchange_rate": null, "description": "Order #42", "settled_at": "2026-06-02T12:31:42.000Z", "expires_at": null, "metadata": { "order_id": "42" } } }

Use the envelope-level id field for idempotency on your side. If you see the same id twice, treat the second delivery as a no-op.

That’s the happy path. Below is the deeper reference you’ll want once you go to production.


Authentication

Every request to the Payments API uses one of two auth modes:

ModeHeaderWhen to use
Service API Keyx-api-key: amb_live_… or amb_test_…Backend-to-API calls. Scoped to specific resources.
Dashboard JWTAuthorization: Bearer <token> (token value is read from your logged-in dashboard session cookie)Master-account-only operations: creating environments, creating or revoking API keys.

The same endpoint accepts both. Most production traffic should use API keys with the minimum permissions required.

Permissions reference

ResourceWhat it gates
ENVIRONMENTSpayment.environment.*: create, delete, find environments
WALLETSpayment.wallet.*: create, delete, find wallets
PAYMENTSpayment.transaction.*: create invoices, list transactions
WEBHOOKSpayment.webhook_endpoint.* and payment.webhook_event.*: manage endpoints, inspect deliveries

Each resource accepts READ or WRITE. WRITE implies READ. Grant the minimum set.

API key management is JWT-only by design. An API key cannot list, create, or revoke other API keys. That surface requires master-account dashboard auth, which blocks privilege-escalation if a key is ever leaked.


Sandbox vs Live

SANDBOXLIVE
API key prefixamb_test_…amb_live_…
Lightning node requiredNoYes (attached to the environment)
Real fundsNo (simulated settlement)Yes
Webhook deliveryYes (same shape)Yes
Use forDevelopment, CI, integration testsProduction traffic

Build in sandbox until your end-to-end flow works. Switching to live is a matter of creating a LIVE environment, attaching a node, and minting a new key.


Available assets

The platform supports BTC plus a growing list of Taproot Asset stablecoins (USDT, USDC at launch). Query the live list at runtime; don’t hard-code symbols.

list-assets.gql
query SupportedAssets { taproot_assets { list { id symbol precision } } }

For BTC, use the BTC BASE_ASSET id you’ll see in the same list (type: BASE_ASSET).


Verifying webhook signatures

Every webhook delivery includes a signature header derived from the endpoint’s secret. The current verification recipe is published in the dashboard alongside the secret. Follow the snippet shown there.

Idempotency on your side. Webhooks may be retried on transient failure or duplicated under network partition. Always dedupe on the envelope id (e.g. payment.completed:tx_…). It is stable across retries.


Lifecycle: a transaction’s status

The corresponding webhook event types are payment.pending, payment.completed, payment.expired, and payment.failed. Subscribe to whichever subset your flow cares about via event_filters on the webhook endpoint.


Error handling tips

  • Auth errors. Missing or invalid key returns UNAUTHENTICATED. A valid key that lacks the required permission returns FORBIDDEN. Both are GraphQL errors with extensions.code.
  • Sandbox routing. Sandbox invoices auto-settle without traversing Lightning. Useful for tests, no node setup required.
  • Live routing failures. If the attached node has no inbound liquidity for the invoice amount, the payer’s wallet will report a path-finding error. Open or rebalance a channel toward the node and retry.
  • Amounts are strings. Always serialize as decimal strings, never JS numbers. The API rejects floats and accepts string-encoded integers in minor units.

Next steps