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:

PathFieldUse when
BOLT11 invoicerequest: { bolt11 }You have an invoice from the recipient. Amount and description are encoded in the invoice.
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
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
        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 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.
  • 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 | 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" }),
}

completepayment.completed, failpayment.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