Get API key

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.

Team password and payment execution

Every SDK send requires a non-empty password. Live sends need the real team password. In sandbox, any non-empty value such as Password123 works because the password is not used. For a live wallet, the SDK completes the send client-side:

  1. It calls create_send to create the transaction and obtain its payment request.
  2. It derives the master key from the password, decrypts the team's symmetric key, then uses that key to decrypt the wallet's admin macaroon in-process.
  3. It calls the node directly with that macaroon: the LND SendPaymentV2 RPC (the equivalent of lncli payinvoice, exposed over REST as POST /v2/router/send) for BTC, or the litd SendPayment RPC (exposed over REST as POST /v1/taproot-assets/channels/send-payment) for Taproot Assets.
  4. It streams updates until the node reports SUCCEEDED or FAILED.

The plaintext team password never leaves your process. Sandbox has no node RPC; the SDK validates only that password is defined and non-empty so the code path has the same call shape you will use in production.

The raw GraphQL create_send mutation does not accept the plaintext password and does not perform the live node RPC for you. A low-level client must implement the same credential-decryption and node-payment steps. The SDK handles that complete flow.

Three send paths

Exactly one of request, address, or destination must be provided:

PathFieldUse when
BOLT11 invoicerequest: { bolt11, amount? }You have an invoice from the recipient. Amount and description are encoded in the invoice. For a zero-amount invoice, you specify the amount.
Lightning Addressaddress: { lightning_address, amount }You only have a Lightning Address ([email protected]). You specify how much to send.
Saved destinationdestination: { payment_destination_id, amount }You saved the recipient as a payment destination. You specify how much to send.

Input fields

FieldTypeRequiredNotes
wallet_idUUIDyesThe wallet the funds come from
request.bolt11string (1 – 2048)one ofBOLT11 invoice string
request.amountstringfor zero-amount invoicesPositive integer in sats. Required when the invoice has no amount. Rejected when the invoice encodes an amount.
address.lightning_addressstring (1 – 255)one of[email protected] format
address.amountstringwith addressPositive integer in the wallet asset's base unit
destination.payment_destination_idUUIDone ofA saved payment destination
destination.amountstringwith destinationPositive integer in the wallet asset's base unit
idempotency_keystring (1 – 255)noReplays return the original transaction
metadatastring (JSON, ≤2048)noSender-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
        payment_request
        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 payment_request } } } }",
    "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
          payment_request
        }
      }
    }
  }
`;

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
        payment_request
      }
    }
  }
}
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 with Invoice network … is not allowed for this wallet.
  • Sandbox wallets accept invoices on any network — useful for testing against your own infrastructure or public testnets.
  • Zero-amount invoices need request.amount (in sats). A send without it is rejected with Invoice has no amount; provide `request.amount`. Taproot Asset wallets also take the amount in sats and pay it in the asset at the current exchange rate.
  • Invoices that encode an amount reject request.amount, even when it is equal. The amount always comes from the invoice.
  • 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.

Paying your own invoice

A team can pay an invoice that one of its own wallets issued, from another of its wallets. For example, a USDT (Taproot Asset) wallet issues an invoice, and the BTC wallet in the same environment pays it. The team's own receive for the invoice does not count as a duplicate.

create_send rejects a send only when the team already has a pending or completed send for the same payment hash, so the same invoice cannot be paid twice:

A payment with this payment hash is already pending or completed.

Paying your own invoice is off by default. To turn it on:

  • SDK: pass allowSelfPayment: true to send. To retry a failed self-payment, pass it again: retryPayment(paymentId, { allowSelfPayment: true }).
  • Dashboard: under Advanced, tick Allow paying my own invoice before you send or retry.

Lifecycle

Send transactions follow the same status machine as receive:

PENDING → COMPLETED | FAILED

The 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
        }
      }
    }
  }
}
  • error is null on success and populated with a human-readable reason on FAILED (e.g. routing failure, insufficient liquidity, invoice already paid).
  • events is the ordered state-transition log. details carries provider-specific context (route attempts, HTLC failures); use it when filing a support ticket.
  • exchange_rate is set for Taproot Asset sends once settled, null for 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 progress

If 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 FAILED via find_one — only then originate a new attempt with a new idempotency_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_key

Sandbox 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.

Next steps