Prompt for Agents
A copy-paste prompt that gives Claude Code, Codex, Cursor, or any coding agent everything it needs to integrate the Amboss Payments GraphQL API correctly.
Working with a coding agent? Paste the prompt below into Claude Code, Cursor, Codex, Copilot, or whatever you use. It carries the endpoint, both auth modes, the exact mutation shapes, and the handful of rules agents reliably get wrong — string amounts in minor units, the sandbox expire default, raw-body webhook verification, and idempotency on sends.
Integrate the Payments API
PromptWires up the GraphQL API end to end: client, wallet lookup, receive, send, and a verified webhook handler.
# Task: integrate Amboss Payments (GraphQL API)
You are integrating Amboss Payments into this codebase. Amboss Payments settles
USDT, USDC, and BTC over the Lightning Network with sub-second finality and no
chargebacks. Everything runs through one GraphQL endpoint.
Endpoint: https://app.amboss.tech/graphql
Docs: https://docs.amboss.tech/payments
Work in the sandbox environment until the whole flow passes end to end. Do not
touch live keys or live wallets unless I explicitly ask you to.
## Rules you must not break
1. **Amounts are decimal strings in the asset's minor units.** `"100000"` sats
for BTC (precision 8), `"1000000"` for 1 USDT (precision 6). Never use a
JavaScript number, never use a float. The API rejects them.
2. **Never hard-code an `asset_id` or a symbol list.** Discover assets at
runtime with the `taproot_assets { list { id symbol precision } }` query.
BTC is the entry with `type: BASE_ASSET`.
3. **In sandbox, `metadata.amb_sandbox_behavior` decides the outcome, and it
defaults to `expire`.** Omit it and your test invoice times out instead of
settling. Use `"complete"`, `"fail"`, or `"expire"` deliberately.
4. **Verify every webhook before acting on it**, over the *raw* request body.
See the webhook step below.
5. **Dedupe on the webhook envelope `id`** (e.g.
`payment.completed:tx_01HX9…`). Delivery is at-least-once, up to 8
attempts, and the id is stable across retries.
6. **Send `idempotency_key` on every `create_send`.** A retry without one can
pay twice.
7. **Secrets never touch source, logs, or fixtures.** `plaintext_key` and the
webhook `secret` are each returned exactly once at creation. Read them from
environment variables and store them in whatever secret manager this repo
already uses.
## Environment variables to add
Add these to the repo's env schema / `.env.example` using its existing
conventions. Never commit real values.
- `AMBOSS_API_KEY` — service API key (`amb_test_…` in sandbox, `amb_live_…` in live)
- `AMBOSS_ENVIRONMENT_ID` — the payments environment UUID
- `AMBOSS_WALLET_ID` — the wallet the integration receives into
- `AMBOSS_WEBHOOK_SECRET` — the endpoint secret used to verify deliveries
- `AMBOSS_GRAPHQL_URL` — defaults to `https://app.amboss.tech/graphql`
## Authentication
Two modes hit the same endpoint:
| Mode | Header | Use for |
|---|---|---|
| Service API key | `x-api-key: amb_test_…` | All backend traffic: wallets, transactions, webhooks |
| Dashboard JWT | `Authorization: Bearer <token>` | Master-account only: creating environments, minting or revoking API keys |
Creating environments and API keys is a **master-account, dashboard-auth
operation**. Do not build that into the application backend — assume I have
already created the environment and minted the key, and read them from the env
vars above. Key permissions are scoped per resource
(`ENVIRONMENTS`, `WALLETS`, `PAYMENTS`, `WEBHOOKS`) at `READ` or
`WRITE`; `WRITE` implies `READ`.
## Build this
### 1. A thin GraphQL client
One small module that POSTs `{ query, variables }` to the endpoint with the
`x-api-key` header, and throws a typed error when the response carries a
`errors` array. Map `extensions.code`: `UNAUTHENTICATED` means the key is
missing or wrong, `FORBIDDEN` means the key lacks the permission. Reuse this
repo's existing HTTP client and error types if it has them — do not add a
GraphQL library for four operations.
### 2. Wallet lookup
```graphql
query Wallet($id: String!) {
payment {
wallet {
find_one(id: $id) {
id
name
is_ready
asset { id symbol precision }
balance { balance received sent }
}
}
}
}
```
Use `precision` to convert between your display amounts and minor units. Do
not assume 8 or 6. `is_ready` is always `true` in sandbox; on a live wallet it
flips to `true` only once liquidity has been provisioned (around 30 minutes),
so guard invoice creation on it rather than letting the payment fail to route.
### 3. Receiving a payment
```graphql
mutation CreateReceive($input: CreateReceiveTransactionInput!) {
payment {
transaction {
create_receive(input: $input) {
id
status
payment_request
payment_hash
expires_at
amount { display_amount full_amount }
}
}
}
}
```
```json
{
"input": {
"wallet_id": "<AMBOSS_WALLET_ID>",
"amount": "100000",
"description": "Order #42",
"expires_in_seconds": 3600,
"idempotency_key": "order-42-attempt-1",
"metadata": "{\"order_id\":\"42\",\"amb_sandbox_behavior\":\"complete\"}"
}
}
```
`metadata` is a **JSON-encoded string**, not an object. It round-trips onto
the webhook payload, so put your order id in it — that is how you correlate a
settlement back to your own records.
Return `payment_request` (a BOLT11 invoice) to the client and render it as a
QR code or a `lightning:` link. Store `id`, `payment_hash`, and
`expires_at` against your order.
### 4. Sending a payment
```graphql
mutation CreateSend($input: CreateSendTransactionInput!) {
payment {
transaction {
create_send(input: $input) { id status payment_hash }
}
}
}
```
Destination is either `request: { bolt11 }` or
`address: { lightning_address, amount }`. Always pass `idempotency_key`.
Important retry semantics: a `FAILED` send is **permanent under its
idempotency key**. Replaying the same `(wallet_id, idempotency_key)` returns
that same `FAILED` record forever — it never re-attempts. But do not
reflexively mint a fresh key after a client-side error either; the original
send may actually be `PENDING` or `COMPLETED`, and a new key would pay twice.
Read the transaction status first, then decide.
### 5. The webhook handler
Register the endpoint once (this is setup, not application code):
```graphql
mutation CreateWebhookEndpoint($input: CreateWebhookEndpointInput!) {
payment {
webhook_endpoint {
create(input: $input) {
secret
endpoint { id url event_filters active }
}
}
}
}
```
Event types: `payment.pending`, `payment.completed`, `payment.failed`,
`payment.expired`. An empty or omitted `event_filters` means all of them.
Then build the handler:
- Capture the **raw body bytes before any JSON parser runs**. In Express that
is `express.raw({ type: "application/json" })`; in NestJS it is
`NestFactory.create(AppModule, { rawBody: true })`; with the Fetch API it is
`await request.text()`. A re-serialized body will not match the HMAC.
- Compute the expected signature as
`HMAC_SHA256(secret, timestamp + "." + rawBody)`, hex-encode it, and compare
it against the `x-webhook-signature` header with a constant-time comparison
(`crypto.timingSafeEqual`). Never `===`.
- Reject when `x-webhook-timestamp` is more than 300 seconds from now.
- Dedupe on the envelope `id` before applying any side effect.
- Respond `200` fast. Do the real work on a queue. Non-2xx and timeouts are
retried at 10s, then 60s, then 10 minutes.
Payload shape:
```json
{
"id": "payment.completed:tx_01HX9YQK7P8MVZ3FN4G2RWS6CD",
"event_type": "payment.completed",
"environment": "sandbox",
"wallet_id": "5e4b1e2a-…",
"data": {
"id": "tx_01HX9YQK7P8MVZ3FN4G2RWS6CD",
"direction": "receive",
"status": "completed",
"amount": { "amount": "100000", "asset_symbol": "USDT", "precision": 6 },
"fee": { "amount": "5", "asset_symbol": "USDT", "precision": 6 },
"settle_amount": { "amount": "99995", "asset_symbol": "USDT", "precision": 6 },
"settled_at": "2026-06-02T12:31:42.000Z",
"metadata": { "order_id": "42" }
}
}
```
Credit the order against `settle_amount`, not `amount` — the fee comes out
in between. Branch on `data.direction` so a send and a receive are not
confused.
## Before you tell me you are done
- [ ] No secret, key, or macaroon appears in source, logs, tests, or fixtures.
- [ ] Every amount in the diff is a string in minor units, derived from the
asset's `precision`.
- [ ] The webhook handler verifies over raw bytes, compares in constant time,
checks the timestamp window, and dedupes on the envelope `id`.
- [ ] Every `create_send` passes an `idempotency_key`.
- [ ] A sandbox receive with `amb_sandbox_behavior: "complete"` settles and
the handler credits the order exactly once, including on a replayed
delivery.
- [ ] The repo's existing lint, typecheck, and test commands pass. Show me the
output.
Follow this repo's existing conventions for structure, naming, error handling,
and tests. Do not add dependencies that the codebase does not already have.
## Reference
- Payments overview — https://docs.amboss.tech/payments
- Full API walkthrough — https://docs.amboss.tech/payments/integrate
- Receive payments — https://docs.amboss.tech/payments/receive-payments
- Send payments — https://docs.amboss.tech/payments/send-payments
- Webhooks — https://docs.amboss.tech/payments/webhooks
- Verify webhooks — https://docs.amboss.tech/payments/verify-webhooks
- Environments — https://docs.amboss.tech/payments/environments
- API keys — https://docs.amboss.tech/payments/api-keys
- Machine-readable index of all docs — https://docs.amboss.tech/llms.txt
Building in TypeScript? Use the SDK prompt instead. It targets @ambosstech/payments, so your agent writes typed method calls rather than hand-rolled GraphQL.
How to use it
Paste it into your agent
Copy the prompt and drop it into your agent's session, in the repo where the integration should land. It's written to be self-contained — no other page needs to be open.
Add your own context
Tell it what you're building ("add a checkout that invoices in USDT") and point it at the module that should own the integration. The prompt tells the agent to follow your repo's existing conventions rather than inventing new ones.
Keep it in sandbox
The prompt already instructs the agent to stay in sandbox and to set amb_sandbox_behavior deliberately rather than letting invoices default to expire. Keep live keys out of the session until the flow passes end to end.
Walk the self-check
The prompt closes with a checklist — no leaked secrets, string amounts, verified webhooks, idempotent sends. Ask the agent to walk it item by item and show you the lint, typecheck, and test output.
Feeding docs to your agent
Everything on this site is also available as plain text, for agents that fetch rather than browse:
| Resource | What it is |
|---|---|
/prompts/payments-api.md | This prompt, as raw Markdown |
/prompts/payments-sdk.md | The TypeScript SDK prompt, as raw Markdown |
/llms.txt | A machine-readable index of every page on this site |
Point an agent at /llms.txt when it needs to look something up mid-task, and at a prompt when it needs to know how to build.
Review what your agent writes before you ship it. The prompt tells it to keep API keys and webhook secrets in environment variables and out of source, but you own the final diff — check for leaked keys, and confirm the webhook handler verifies signatures over the raw body.
What the prompt covers
| Area | What the agent is told |
|---|---|
| Auth | x-api-key for backend traffic, dashboard JWT for master-account operations, and that key creation must not live in application code |
| Amounts | Decimal strings in minor units, derived from the asset's precision — never a JS number |
| Assets | Discover with taproot_assets, never hard-code an asset_id or a symbol list |
| Receiving | The create_receive shape, JSON-encoded metadata, and correlating settlement back to your order |
| Sending | The create_send shape, plus the failed-send idempotency trap that causes double payments |
| Webhooks | Raw-body HMAC, constant-time comparison, timestamp tolerance, envelope-id dedupe, fast 200s |
| Money | Credit settle_amount, not amount — the fee sits between them |