MenuPayment API

Revenue attribution

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

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.

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.

Example

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

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": "[email protected]", "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 }
  }'
// Node 18 or later: fetch is built in, no SDK needed.
const payment = {
  payment_id: 'inv_1',
  customer: { id: 'cus_1', email: '[email protected]', 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}`);
import os
import requests

payment = {
    "payment_id": "inv_1",
    "customer": {"id": "cus_1", "email": "[email protected]", "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. Errors are listed on Errors.

    ↑ ↓ to move, Enter to open