# Payment API

Send payments from any billing system ProductLense has no connector for, and they are attributed exactly like Stripe or Paddle payments.

The Payment API takes payments that ProductLense cannot pull itself: invoices paid by bank transfer, an in-house billing system, app store revenue exported by a script, or a provider without a connector. Each payment you send goes through the same pipeline as a connector payment: it is converted to USD, its net revenue is computed, and it is attributed to the first touch of the visitor who became the customer.

## When to use it

- Your billing provider is not one of the [ten connectors](/docs/revenue/connect-billing).
- You invoice some customers by hand and want that revenue next to the rest.
- You want full control over what ProductLense sees, one payment at a time.

If your provider has a connector, use the connector instead: it backfills twelve months and keeps up through webhooks without code.

> [!NOTE]
> The Payment API needs an API key with the `payments` scope. API keys are part of the Scale plan. See [Authentication and API keys](/docs/api/authentication).

## How it works

1. Create an API key with the `payments` scope in Settings → API keys.
2. From your server, send one `POST https://api.productlense.app/api/v1/payments` per payment, charge, refund or chargeback.
3. ProductLense stores the customer, the payment and, when you include it, the subscription. The first request creates a connection called Payment API on the site; it appears with your other billing connections.
4. The payment is attributed when it is written. If the customer has not been identified yet, a daily job retries for 90 days. See [How payments are attributed](/docs/revenue/attribution).

Only payments with status `succeeded` (the default) count toward revenue. `pending` and `failed` payments are stored but left out of every revenue figure until you send the same `payment_id` again with `succeeded`.

## Idempotency

`payment_id` is the idempotency key. Sending a payment with a `payment_id` ProductLense already has updates the stored payment instead of adding a second one, so retries after a timeout, nightly re-syncs and status changes are safe.

## Refunds and chargebacks

A refund is its own payment with its own `payment_id`, `kind: "refund"` and a positive `amount`. A chargeback uses `kind: "chargeback"`. Use the same `customer.id` as the original charge so the refund lands on the same channel. Net revenue goes down by the refunded amount plus any fee you send on the refund; see [Refunds, fees and currencies](/docs/revenue/refunds-fees-currencies).

## Example

A monthly charge of 29.00 USD with a 1.15 USD processor fee, linked to the user id you pass to `identify`:

```bash tab="cURL"
curl -X POST https://api.productlense.app/api/v1/payments \
  -H "Authorization: Bearer $PL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "payment_id": "inv_1",
    "customer": { "id": "cus_1", "email": "ada@example.com", "user_id": "user_123" },
    "amount": 29.00,
    "currency": "USD",
    "fee": 1.15,
    "kind": "charge",
    "paid_at": "2026-10-11T10:00:00Z",
    "subscription": { "id": "sub_1", "plan": "Pro", "interval": "month", "status": "active", "amount": 29.00 }
  }'
```

```js tab="Node"
// Node 18 or later: fetch is built in, no SDK needed.
const payment = {
  payment_id: 'inv_1',
  customer: { id: 'cus_1', email: 'ada@example.com', user_id: 'user_123' },
  amount: 29.0,
  currency: 'USD',
  fee: 1.15,
  kind: 'charge',
  paid_at: '2026-10-11T10:00:00Z',
  subscription: { id: 'sub_1', plan: 'Pro', interval: 'month', status: 'active', amount: 29.0 },
};

const res = await fetch('https://api.productlense.app/api/v1/payments', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.PL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(payment),
});
const body = await res.json();
if (!body.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
```

```python tab="Python"
import os
import requests

payment = {
    "payment_id": "inv_1",
    "customer": {"id": "cus_1", "email": "ada@example.com", "user_id": "user_123"},
    "amount": 29.00,
    "currency": "USD",
    "fee": 1.15,
    "kind": "charge",
    "paid_at": "2026-10-11T10:00:00Z",
    "subscription": {"id": "sub_1", "plan": "Pro", "interval": "month", "status": "active", "amount": 29.00},
}

res = requests.post(
    "https://api.productlense.app/api/v1/payments",
    json=payment,
    headers={"Authorization": f"Bearer {os.environ['PL_API_KEY']}"},
    timeout=10,
)
body = res.json()
if not body["ok"]:
    raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
```

A successful request answers `200` with `{ "ok": true, "data": { "payment_id": "inv_1" } }`.

## Field reference

Every field, its rules, the exact validation messages and a Go example are on [Payments API](/docs/api/payments). Errors are listed on [Errors](/docs/api/errors).
