Integrate the Payments API
End-to-end Amboss Payments walkthrough — API key, environment, wallet, invoice, and first received payment in BTC or stablecoin.
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 page is the end-to-end walkthrough. For focused references, see Environments, Wallets, API Keys, Receive Payments, Send Payments, and Webhooks.
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.
Working in TypeScript? The official SDK wraps this same API in a typed client, so you can do the whole flow below without writing GraphQL by hand.
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.
mutation CreateEnvironment {
payment {
environment {
create(input: {
name: "Sandbox - dev"
type: SANDBOX
}) {
id
name
type
created_at
}
}
}
}curl -X POST https://rails.amboss.tech/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $AMBOSS_DASHBOARD_TOKEN" \
-d '{
"query": "mutation { payment { environment { create(input: { name: \"Sandbox - dev\", type: SANDBOX }) { id name type created_at } } } }"
}'const res = await fetch("https://rails.amboss.tech/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.AMBOSS_DASHBOARD_TOKEN}`,
},
body: JSON.stringify({
query: `
mutation CreateEnvironment($input: CreatePaymentsEnvironmentInput!) {
payment {
environment {
create(input: $input) {
id
name
type
created_at
}
}
}
}
`,
variables: { input: { name: "Sandbox - dev", type: "SANDBOX" } },
}),
});
const { data } = await res.json();
console.log(data.payment.environment.create.id);{
"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).
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 }
}
}
}
}curl -X POST https://rails.amboss.tech/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $AMBOSS_DASHBOARD_TOKEN" \
-d '{
"query": "mutation($input: CreateServiceApiKeyInput!) { service_api_key { create(input: $input) { plaintext_key key { id name key_prefix permissions { resource level } } } } }",
"variables": {
"input": {
"name": "backend-prod",
"environment_id": "81b73615-ddf3-46e3-943a-467c3e442e04",
"permissions": [
{ "resource": "WALLETS", "level": "WRITE" },
{ "resource": "PAYMENTS", "level": "WRITE" },
{ "resource": "WEBHOOKS", "level": "WRITE" }
]
}
}
}'const res = await fetch("https://rails.amboss.tech/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.AMBOSS_DASHBOARD_TOKEN}`,
},
body: JSON.stringify({
query: `
mutation CreateApiKey($input: CreateServiceApiKeyInput!) {
service_api_key {
create(input: $input) {
plaintext_key
key { id name key_prefix permissions { resource level } }
}
}
}
`,
variables: {
input: {
name: "backend-prod",
environment_id: "81b73615-ddf3-46e3-943a-467c3e442e04",
permissions: [
{ resource: "WALLETS", level: "WRITE" },
{ resource: "PAYMENTS", level: "WRITE" },
{ resource: "WEBHOOKS", level: "WRITE" },
],
},
},
}),
});
const { data } = await res.json();
const apiKey = data.service_api_key.create.plaintext_key;
// store this securely; it will never be shown again{
"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.
query AvailableAssets {
taproot_assets {
list {
id
symbol
precision
}
}
}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 }
}
}
}
}curl -X POST https://rails.amboss.tech/graphql \
-H "Content-Type: application/json" \
-H "x-api-key: $AMBOSS_API_KEY" \
-d '{
"query": "mutation($input: CreatePaymentsWalletInput!) { payment { wallet { create(input: $input) { id name asset { symbol precision } balance { balance received sent } } } } }",
"variables": {
"input": {
"name": "Main USDT wallet",
"environment_id": "81b73615-ddf3-46e3-943a-467c3e442e04",
"asset_id": "<asset_id_from_above>"
}
}
}'const res = await fetch("https://rails.amboss.tech/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": process.env.AMBOSS_API_KEY,
},
body: JSON.stringify({
query: `
mutation CreateWallet($input: CreatePaymentsWalletInput!) {
payment {
wallet {
create(input: $input) {
id
name
asset { symbol precision }
balance { balance received sent }
}
}
}
}
`,
variables: {
input: {
name: "Main USDT wallet",
environment_id: "81b73615-ddf3-46e3-943a-467c3e442e04",
asset_id: process.env.AMBOSS_ASSET_ID_USDT,
},
},
}),
});
const { data } = await res.json();
console.log(data.payment.wallet.create.id);{
"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.
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 }
}
}
}
}curl -X POST https://rails.amboss.tech/graphql \
-H "Content-Type: application/json" \
-H "x-api-key: $AMBOSS_API_KEY" \
-d '{
"query": "mutation($input: CreateReceiveTransactionInput!) { payment { transaction { create_receive(input: $input) { id status payment_request payment_hash expires_at amount { display_amount full_amount } } } } }",
"variables": {
"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\"}"
}
}
}'const res = await fetch("https://rails.amboss.tech/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": process.env.AMBOSS_API_KEY,
},
body: JSON.stringify({
query: `
mutation CreateReceive($input: CreateReceiveTransactionInput!) {
payment {
transaction {
create_receive(input: $input) {
id
status
payment_request
payment_hash
expires_at
amount { display_amount full_amount }
}
}
}
}
`,
variables: {
input: {
wallet_id: walletId,
amount: "100000",
description: "Order #42",
expires_in_seconds: 3600,
idempotency_key: `order-${orderId}-attempt-${attempt}`,
metadata: JSON.stringify({ order_id: orderId }),
},
},
}),
});
const { data } = await res.json();
// Show data.payment.transaction.create_receive.payment_request to the customer{
"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 PENDING → COMPLETED.
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.
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
}
}
}
}
}curl -X POST https://rails.amboss.tech/graphql \
-H "Content-Type: application/json" \
-H "x-api-key: $AMBOSS_API_KEY" \
-d '{
"query": "mutation($input: CreateWebhookEndpointInput!) { payment { webhook_endpoint { create(input: $input) { secret endpoint { id url event_filters active } } } } }",
"variables": {
"input": {
"environment_id": "81b73615-ddf3-46e3-943a-467c3e442e04",
"url": "https://api.example.com/webhooks/amboss",
"event_filters": ["payment.completed", "payment.failed", "payment.expired"]
}
}
}'const res = await fetch("https://rails.amboss.tech/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": process.env.AMBOSS_API_KEY,
},
body: JSON.stringify({
query: `
mutation CreateWebhookEndpoint($input: CreateWebhookEndpointInput!) {
payment {
webhook_endpoint {
create(input: $input) {
secret
endpoint { id url event_filters active }
}
}
}
}
`,
variables: {
input: {
environment_id: envId,
url: "https://api.example.com/webhooks/amboss",
event_filters: ["payment.completed", "payment.failed", "payment.expired"],
},
},
}),
});
const { data } = await res.json();
// Save data.payment.webhook_endpoint.create.secret in your secret manager{
"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:
{
"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:
| Mode | Header | When to use |
|---|---|---|
| Service API Key | x-api-key: amb_live_… or amb_test_… | Backend-to-API calls. Scoped to specific resources. |
| Dashboard JWT | Authorization: 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
| Resource | What it gates |
|---|---|
ENVIRONMENTS | payment.environment.*: create, delete, find environments |
WALLETS | payment.wallet.*: create, delete, find wallets |
PAYMENTS | payment.transaction.*: create invoices, list transactions |
WEBHOOKS | payment.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
SANDBOX | LIVE | |
|---|---|---|
| API key prefix | amb_test_… | amb_live_… |
| Lightning node required | No | Yes (attached to the environment) |
| Real funds | No (simulated settlement) | Yes |
| Webhook delivery | Yes (same shape) | Yes |
| Use for | Development, CI, integration tests | Production 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.
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 returnsFORBIDDEN. Both are GraphQL errors withextensions.code. - Sandbox routing. Sandbox invoices auto-settle without traversing Lightning. Useful for tests, no node setup required.
- Live routing failures. If the wallet doesn't have enough liquidity provisioned for the invoice amount, the payment can't route. Wait for provisioning to complete (check
is_readyon the wallet) 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.