Send Payments
Pay outbound Lightning invoices, Lightning Addresses, and saved payment destinations programmatically from any Amboss wallet.
Pay an outbound Lightning invoice, Lightning Address, or saved payment destination from one of your wallets.
payment.transaction.create_send(input: CreateSendTransactionInput) → PaymentsTransaction
Equivalent nested GraphQL syntax:
mutation {
payment {
transaction {
create_send(input: { … }) { … }
}
}
}Using TypeScript? payments.transactions.send in the SDK handles both BOLT11 and Lightning Address sends, picks the node endpoint automatically, and streams live progress.
Three send paths
Exactly one of request, address, or destination must be provided:
| Path | Field | Use when |
|---|---|---|
| BOLT11 invoice | request: { bolt11 } | You have an invoice from the recipient. Amount and description are encoded in the invoice. |
| Lightning Address | address: { lightning_address, amount } | You only have a Lightning Address ([email protected]). You specify how much to send. |
| Saved destination | destination: { payment_destination_id, amount } | You saved the recipient as a payment destination. You specify how much to send. |
Input fields
| Field | Type | Required | Notes |
|---|---|---|---|
wallet_id | UUID | yes | The wallet the funds come from |
request.bolt11 | string (1 – 2048) | one of | BOLT11 invoice string |
address.lightning_address | string (1 – 255) | one of | [email protected] format |
address.amount | string | with address | Positive integer in the wallet asset's base unit |
destination.payment_destination_id | UUID | one of | A saved payment destination |
destination.amount | string | with destination | Positive integer in the wallet asset's base unit |
idempotency_key | string (1 – 255) | no | Replays return the original transaction |
metadata | string (JSON, ≤2048) | no | Sender-side annotation. The receiver-visible description lives on the BOLT11. |
Pay a BOLT11 invoice
mutation SendBolt11 {
payment {
transaction {
create_send(input: {
wallet_id: "5e4b1e2a-9f3c-4a5b-8c7d-1234567890ab"
request: { bolt11: "lnbc100u1p3xxxxxxxx..." }
idempotency_key: "payout-2026-06-01-42"
metadata: "{\"payout_id\":\"42\"}"
}) {
id
status
payment_hash
amount { full_amount }
}
}
}
}curl -X POST https://app.amboss.tech/graphql \
-H "Content-Type: application/json" \
-H "x-api-key: $AMBOSS_API_KEY" \
-d '{
"query": "mutation($input: CreateSendTransactionInput!) { payment { transaction { create_send(input: $input) { id status payment_hash } } } }",
"variables": {
"input": {
"wallet_id": "5e4b1e2a-9f3c-4a5b-8c7d-1234567890ab",
"request": { "bolt11": "lnbc100u1p3xxxxxxxx..." },
"idempotency_key": "payout-2026-06-01-42"
}
}
}'import { GraphQLClient, gql } from "graphql-request";
const client = new GraphQLClient("https://app.amboss.tech/graphql", {
headers: { "x-api-key": process.env.AMBOSS_API_KEY },
});
const CREATE_SEND = gql`
mutation CreateSend($input: CreateSendTransactionInput!) {
payment {
transaction {
create_send(input: $input) {
id
status
payment_hash
}
}
}
}
`;
const { payment } = await client.request(CREATE_SEND, {
input: {
wallet_id: walletId,
request: { bolt11: invoiceString },
idempotency_key: `payout-${payoutId}`,
metadata: JSON.stringify({ payout_id: payoutId }),
},
});Pay a Lightning Address
mutation SendToAddress {
payment {
transaction {
create_send(input: {
wallet_id: "5e4b1e2a-9f3c-4a5b-8c7d-1234567890ab"
address: {
lightning_address: "[email protected]"
amount: "50000"
}
idempotency_key: "tip-2026-06-01-alice"
}) {
id
status
}
}
}
}await client.request(CREATE_SEND, {
input: {
wallet_id: walletId,
address: {
lightning_address: "[email protected]",
amount: "50000", // base units
},
idempotency_key: `tip-${tipId}`,
},
});Lightning Address sends from Taproot Asset wallets resolve the address via LNURL, then convert the invoice amount to asset units using an RFQ quote before sending. This applies to saved destinations too, since they resolve to Lightning Addresses.
Pay a saved destination
For recipients you pay repeatedly, save the Lightning Address as a payment destination once and send by id:
mutation SendToDestination {
payment {
transaction {
create_send(input: {
wallet_id: "5e4b1e2a-9f3c-4a5b-8c7d-1234567890ab"
destination: {
payment_destination_id: "c2f9d8e1-4b6a-4f3c-9d2e-0987654321fe"
amount: "50000"
}
idempotency_key: "payout-2026-07-03-alice"
}) {
id
status
}
}
}
}await client.request(CREATE_SEND, {
input: {
wallet_id: walletId,
destination: {
payment_destination_id: destinationId,
amount: "50000", // base units
},
idempotency_key: `payout-${payoutId}`,
},
});The transaction records the payment_destination_id, keeping payout history linked to the saved destination.
Network constraints
- Live wallets only accept invoices for the production network (mainnet
lnbc…). Invoices for testnet, mutinynet, or regtest are rejected withInvoice network … is not allowed for this wallet. - Sandbox wallets accept invoices on any network — useful for testing against your own infrastructure or public testnets.
- Amountless invoices are not yet supported. The BOLT11 must encode a positive amount.
- Expired invoices are rejected up front (
Invoice has already expired). - The wallet must have an attached node for the asset type. For Taproot Assets the node must have tapd capability.
Lifecycle
Send transactions follow the same status machine as receive:
PENDING → COMPLETED | FAILEDThe payment.completed and payment.failed webhooks fire on terminal transitions. The data.direction field on the envelope is "send". See Webhooks.
Inspecting a send
When a send terminates as FAILED, the failure reason is on the transaction itself — query error and events for the full picture:
query GetSend($id: String!) {
payment {
transaction {
find_one(id: $id) {
id
status
settle_amount { full_amount }
settled_at
exchange_rate
error
events {
event_type
message
details
created_at
}
}
}
}
}errorisnullon success and populated with a human-readable reason onFAILED(e.g. routing failure, insufficient liquidity, invoice already paid).eventsis the ordered state-transition log.detailscarries provider-specific context (route attempts, HTLC failures); use it when filing a support ticket.exchange_rateis set for Taproot Asset sends once settled,nullfor BTC.
Don't use settle_amount_sats. It is deprecated in favor of settle_amount, which carries both the value and the asset metadata. The deprecated field will be removed in a future release.
Idempotency
idempotency_key is highly recommended for sends — a retry without a key can result in a double payment. Replays with the same (wallet_id, idempotency_key) return the original transaction.
Concurrent requests with the same key are serialized:
A request with this idempotency_key is already in progressIf you omit idempotency_key, the server generates one internally per call (so each call is unique by definition). The safer pattern is for you to choose one tied to your business identifier (payout-${id}).
Retrying a failed send
A FAILED transaction is permanent under its idempotency key. Replaying create_send with the same (wallet_id, idempotency_key) always returns that same FAILED transaction — there's no automatic re-attempt, no matter how many times you retry. If your retry logic just resends the same key on failure, it can never recover from a real failure.
There are two distinct retry paths, and picking the wrong one matters:
- You don't know if the original request landed (network timeout, client crashed before reading the response) — retry with the same
idempotency_key. This is exactly what idempotency is for: if Amboss already created the send, you get that transaction back in whatever state it's actually in; if not, a fresh attempt runs. - You've confirmed the send is
FAILEDviafind_one— only then originate a new attempt with a newidempotency_key.
Don't mint a new idempotency_key reflexively after a client-side error without checking status first. The original send might actually be PENDING or COMPLETED — retrying with a new key in that case risks a double payment.
const tx = await findOne(transactionId);
if (tx.status === "FAILED") {
// confirmed dead — safe to originate a new attempt
await createSend({ ...input, idempotency_key: newKey });
}
// PENDING or COMPLETED: do nothing, or retry with the original idempotency_keySandbox testing
Set metadata.amb_sandbox_behavior to drive terminal state in sandbox:
{
wallet_id: walletId,
request: { bolt11: anySandboxInvoice },
metadata: JSON.stringify({ amb_sandbox_behavior: "complete" }),
}complete → payment.completed, fail → payment.failed, and expire (default) → payment.expired — omitting amb_sandbox_behavior behaves like expire, so a send with no flag set times out rather than settling. See Environments for the full table.