> For the complete documentation index, see [llms.txt](https://help.getlfg.app/p/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://help.getlfg.app/p/developers/pay-api-integration-guide.md).

# Pay API integration guide

This is a complete, self-contained guide to the LFG Pay API. It's written so you can read it top to bottom — or paste the whole page into an AI assistant (Claude, ChatGPT) and ask it to build the integration for your framework.

> **Using an AI assistant?** Copy this entire page and prompt it with something like: *"Here are the LFG Pay API docs. Build a 'Pay with LFG' checkout in \<your stack — e.g. Next.js / Laravel / Django>: create a payment link on my server, redirect the buyer to the checkout URL, and verify the settlement webhook."*

## How it works

The flow is three steps:

1. **Create a payment link** — your server calls the API with the amount and an order reference. You get back a `checkoutUrl`.
2. **Redirect the buyer** to that `checkoutUrl`. LFG hosts the checkout, handles the on-chain payment and compliance screening, and returns the buyer to your `successUrl` when they're done.
3. **Receive a webhook** — LFG POSTs a signed event to your server as the payment progresses, so you can fulfil the order automatically.

* **Base URL:** `https://api.getlfg.app/pay/v1`
* **Content type:** `application/json`
* **Field casing:** all request and response fields are `camelCase`.

Everything is scoped to the business your API key belongs to — you never send a business ID, and a leaked key can only ever touch its own business.

## Authentication

Create an API key in your dashboard under **Settings → API Keys**. You'll get two secrets, **shown only once**:

* the **API key** (`lfg_sk_live_…`) — used to authenticate requests.
* the **webhook signing secret** — used to verify webhooks (see below).

Store both in your server's secret manager. They're stored hashed on our side and cannot be retrieved again — if you lose one, revoke the key and create a new one.

Send the API key as a bearer token on every request:

```
Authorization: Bearer lfg_sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

{% hint style="danger" %}
The API key is a **server-side secret**. Never put it in browser code, mobile apps, or any public repository. All API calls must come from your backend.
{% endhint %}

### Authentication errors

| HTTP | `code`              | Meaning                                               |
| ---- | ------------------- | ----------------------------------------------------- |
| 401  | `missing_api_key`   | No `Authorization: Bearer …` header was sent.         |
| 401  | `invalid_api_key`   | The key doesn't match any active key.                 |
| 401  | `api_key_revoked`   | You revoked this key. Create a new one.               |
| 403  | `api_key_disabled`  | LFG disabled the key (see `reason`). Contact support. |
| 403  | `business_disabled` | The business account is disabled. Contact support.    |

`401` means *your credential* is wrong — rotate it. `403` means LFG turned something off — a new key won't help; contact support.

## Rate limits

**60 requests per 60 seconds, per API key.** The limit is per key, so your traffic is never affected by other merchants. Exceeding it returns `429`.

## Response format

Successful responses are wrapped in an envelope:

```json
{
  "data": { ... },
  "meta": { "timestamp": "2026-01-01T12:00:00.000Z" }
}
```

List endpoints add pagination fields (`page`, `limit`, `total`, `totalPages`) under `meta`.

## Create a payment link

```
POST /pay/v1/payment-links
```

### Request body

| Field           | Type   | Required | Notes                                                  |
| --------------- | ------ | -------- | ------------------------------------------------------ |
| `orderId`       | string | yes      | Your own order reference. Echoed back on webhooks.     |
| `fiatAmount`    | number | yes      | The amount to charge (positive).                       |
| `token`         | string | yes      | `USDC` or `USDT`.                                      |
| `network`       | string | yes      | `base` or `ethereum`.                                  |
| `fiatCurrency`  | string | no       | Defaults to your business currency (e.g. `USD`).       |
| `expiryMinutes` | number | no       | How long the checkout stays payable.                   |
| `successUrl`    | string | no       | Where the buyer returns after paying.                  |
| `cancelUrl`     | string | no       | Where the buyer returns on cancel/failure.             |
| `webhookUrl`    | string | no       | Overrides the key's default webhook URL for this link. |
| `skuMetadata`   | object | no       | `{ "items": [ … ] }` line-item detail.                 |

### Example

{% code title="cURL" %}

```bash
curl -X POST https://api.getlfg.app/pay/v1/payment-links \
  -H "Authorization: Bearer $LFG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "orderId": "order_1042",
    "fiatAmount": 49.99,
    "fiatCurrency": "USD",
    "token": "USDC",
    "network": "base",
    "successUrl": "https://yourstore.com/thanks",
    "cancelUrl": "https://yourstore.com/cart"
  }'
```

{% endcode %}

{% code title="Node.js" %}

```javascript
const res = await fetch("https://api.getlfg.app/pay/v1/payment-links", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.LFG_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    orderId: "order_1042",
    fiatAmount: 49.99,
    fiatCurrency: "USD",
    token: "USDC",
    network: "base",
    successUrl: "https://yourstore.com/thanks",
    cancelUrl: "https://yourstore.com/cart",
  }),
});

const { data } = await res.json();
// Redirect the buyer to data.checkoutUrl
```

{% endcode %}

### Response `201`

```json
{
  "data": {
    "id": "…",
    "linkToken": "pay_yourbiz_ab12cd",
    "checkoutUrl": "https://pay.getlfg.app/pay/pay_yourbiz_ab12cd"
  },
  "meta": { "timestamp": "…" }
}
```

Redirect the buyer to `checkoutUrl`. LFG hosts the checkout, the on-chain payment, and compliance screening.

{% hint style="warning" %}
Always recompute the amount on your **server** from your own cart or order — never trust an amount sent from the browser.
{% endhint %}

## List and fetch payment links

```
GET /pay/v1/payment-links?page=1&limit=15
```

Returns your links, newest first, each with its current session, paginated.

```
GET /pay/v1/payment-links/{id}
```

Returns a single link (plus session), or `404` if it isn't yours.

## Returning the buyer to your store

When the payment completes, the buyer sees an **Order Confirmed** screen on the LFG checkout. Closing it returns them to your `successUrl`, with these query parameters appended so you can reconcile the order:

| Param        | Description                    |
| ------------ | ------------------------------ |
| `order_id`   | Your original `orderId`.       |
| `link_token` | The payment link token.        |
| `tx_hash`    | The on-chain transaction hash. |
| `status`     | `success`.                     |

On cancellation or a failed payment (wrong network/asset, underpaid, expired, screening failed), the buyer is returned to your `cancelUrl` with `status=cancelled` / `failed` / `expired`.

{% hint style="info" %}
Treat the **webhook** — not the redirect — as the source of truth for whether a payment succeeded. A buyer can close the tab before returning; the webhook still fires.
{% endhint %}

## Webhooks

If a payment link has a `webhookUrl` (either set per-request, or configured as the key's default), LFG sends a `POST` to it every time the payment session changes state — so you can fulfil orders without polling.

### Payload

```json
{
  "event": "payment.state.changed",
  "orderId": "order_1042",
  "linkToken": "pay_yourbiz_ab12cd",
  "state": "approved",
  "previousState": "screening_in_progress",
  "txHash": "0x…",
  "sourceWallet": "0x…",
  "timestamp": "2026-01-01T12:00:00.000Z"
}
```

Payment states progress roughly as:

```
created → opened → payment_pending → payment_detected
  → screening_in_progress → approved
```

Treat **`approved`** as "paid and cleared" — that's the state at which funds are confirmed and screened. Failure branches include `expired`, `underpaid`, `overpaid`, `wrong_asset`, `wrong_network`, and `screening_failed`.

Delivery is retried up to 3 times with exponential backoff. Return any `2xx` status to acknowledge; a non-2xx triggers a retry.

### Verifying the signature

Every webhook for an API-created link is signed. Each request carries:

* `X-LFG-Timestamp` — unix seconds.
* `X-LFG-Signature` — `HMAC-SHA256(secret, "{timestamp}.{rawBody}")`, hex-encoded.

The `secret` is the **webhook signing secret** you received when creating the API key. Recompute the HMAC over `` `${timestamp}.${rawBody}` `` and compare in constant time. Reject anything with a timestamp older than a few minutes to prevent replays.

{% code title="Node.js" %}

```javascript
import { createHmac, timingSafeEqual } from "crypto";

function verifyWebhook(rawBody, headers, secret) {
  const timestamp = headers["x-lfg-timestamp"];
  const signature = headers["x-lfg-signature"];

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(signature);
  const b = Buffer.from(expected);
  const signatureOk = a.length === b.length && timingSafeEqual(a, b);

  const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
  return signatureOk && fresh;
}
```

{% endcode %}

{% hint style="warning" %}
Verify against the **raw request body**, exactly as received — before any JSON parsing or re-serialising. Re-encoding changes the bytes and breaks the signature.
{% endhint %}

## Managing keys

API keys are created, listed, and revoked by the business **owner** in **Settings → API Keys**. You can have up to **5 active keys** per business — rotate by creating a new key, then revoking the old one. Revoking takes effect immediately.

## Integration checklist

1. Create an API key in **Settings → API Keys**; store the key and webhook secret in your backend.
2. Add a server endpoint that creates a payment link and returns the `checkoutUrl`.
3. Redirect the buyer to `checkoutUrl`; set `successUrl` / `cancelUrl` back to your site.
4. Add a webhook endpoint that verifies the signature and marks the order paid on `approved`.
5. Go live — the API is production from day one; there is no separate test environment.
