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.
GraphQL
mutation CreateEnvironment {
payment {
environment {
create(input: {
name: "Sandbox - dev"
type: SANDBOX
}) {
id
name
type
created_at
}
}
}
}{
"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).
GraphQL
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 }
}
}
}
}{
"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.
GraphQL
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 }
}
}
}
}{
"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.
GraphQL
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 }
}
}
}
}{
"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.
GraphQL
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
}
}
}
}
}{
"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 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.