Source: https://www.revvly.ai/docs/guides/webhook-payment

Guide

# Wire up the payment *webhook*.

Post a payment to Revvly the instant your processor or bank feed sees it land — in full, or a part payment that leaves the rest outstanding. Invoices settle themselves; nobody has to open the dashboard.

## Why this exists

Revvly already invoices on its own schedule — what it cannot see is your bank account or your payment processor's ledger. The invoice payment webhook closes that gap: the moment your PSP, bank feed, or accounting sync confirms money moved, you POST it to https://api.revvly.ai/api/v1/webhooks/invoice-payment and the invoice settles itself. No one has to open the dashboard and click Mark Paid.

- [Invoice concept ›](https://www.revvly.ai/docs/concepts/invoice)

## Get a scoped API key

Issue or reuse an API key under Settings → API Keys and tick Can record payments (the PAYMENT_WRITE capability); without it the webhook answers 403 "API key lacks required capability: PAYMENT_WRITE". Keys that were live on 15 October 2026 already hold it. Two secrets come back the moment you create or rotate one — a Bearer token and a signing secret — both shown once. If your key predates signing secrets, rotate it to provision one; a Bearer token with no signing secret can authenticate but can never pass the signature check. If the key is endpoint-scoped, include /webhooks/invoice-payment (or the trailing-wildcard /webhooks/*) in that scope.

- [API Key concept ›](https://www.revvly.ai/docs/concepts/apiKey)

## Sign the raw body

Compute an HMAC-SHA256 over the exact request bytes, keyed with the signing secret, and send it as X-Signature-SHA256: sha256=<hex>. Sign after you have finished building the JSON — re-serializing it afterwards, even just reordering keys or whitespace, produces different bytes and invalidates the signature. It is the same mechanism the usage-ingestion endpoint uses.

## Point at the right invoice

Identify the invoice with invoiceNumber — the printed number, e.g. ACME-00042, usually the same reference your processor or bank feed already carries — or invoiceId if you already sync Revvly's internal ids. At least one is required; if you send both, invoiceId wins.

- [Invoice concept ›](https://www.revvly.ai/docs/concepts/invoice)

## Full settlement or a part payment

Omit amount and Revvly settles whatever is currently outstanding — the easy path when you know the payment clears the balance. Include amount to record a part payment; it is added to amountPaid and the invoice keeps whatever status it already had — Pending, Overdue, or Failed — still visible in AR and still drawing reminders, until a later call brings the balance to zero. Only an already-Paid or Cancelled invoice refuses a payment outright.

## Make retries free

Idempotency-Key is required on every call — use your processor's own event id. A key already recorded on the invoice is never applied a second time: the response comes back 200 with replayed: true and the invoice's current totals, so your sender sees success and stops retrying instead of double-counting a payment.

## Read the response, then move on

The 200 body is the invoice as it now stands — amountPaid, amountDue, fullyPaid, and appliedAmount (what this specific call moved) — enough to reconcile without a follow-up read. A 409 means the invoice cannot take a payment at all: already paid, cancelled, or nothing left owing. Treat it as a stop, not something to retry later.

What happens to a payment call

1. Signed POSTWith an Idempotency-Key and the invoice number.
2. Amount omittedSettles whatever is outstanding. The invoice becomes Paid.orAmount givenAdded to amountPaid. The invoice keeps its status until the balance is zero.orKey already recorded200 with replayed: true. Nothing is applied again.orAlready paid or cancelled409. Stop, do not retry.

A repeat is acknowledged without applying anything, and a settled invoice is refused.

A worked example

### Acme Corp pays in *two parts*.

One invoice, one partial payment, one full settlement, and a redelivered webhook that changes nothing.

1. 01The invoiceAcme Corp's April cycle produces invoice ACME-00042 for $1,000, due on the 23rd. It sits Pending in Revvly, like every other renewal invoice.
2. 02The charge landsAcme pays $400 by card through your checkout. Your payment processor fires a charge.succeeded webhook carrying event id evt_3PqR8s2eZvKYlo2C.
3. 03You forward itYour handler signs { invoiceNumber: 'ACME-00042', amount: 400, currency: 'USD', reference: 'pi_3PqR8s2eZvKYlo2C' } with your signing secret and POSTs it to https://api.revvly.ai/api/v1/webhooks/invoice-payment with Idempotency-Key: evt_3PqR8s2eZvKYlo2C.
4. 04Revvly applies it200 OK comes back with amountPaid: 400, amountDue: 600, fullyPaid: false. The invoice stays Pending — now correctly showing $600 still owed.
5. 05The balance clearsThree weeks later Acme pays the remaining $600. A second call — new Idempotency-Key, amount omitted this time — settles it: amountPaid: 1000, amountDue: 0, fullyPaid: true, and the invoice flips to Paid.
6. 06A retry costs nothingYour processor redelivers the first webhook a minute after you already processed it. Same Idempotency-Key, same body — Revvly returns replayed: true and applies nothing a second time.

How the $1,000 invoice was paid

- First payment$400
- Second payment$600
- Total$1,000

Two payments add up to the invoice total, and the redelivered webhook adds nothing.

## Give this prompt to an integration agent

Use this context when asking an agent to build a payment-processor forwarding handler.

```
You are forwarding confirmed payments from a PSP, bank feed, or accounting system into Revvly so invoices settle without dashboard work.

Working agreement before you implement:
- Do not write production code until you have enough concrete context from the operator.
- Ask clarifying questions first — prefer one short round of questions over guessing.
- Confirm at least: source system / trigger, field mapping, where the API key and any signing secret will be stored, the target environment (local, staging, or production), and the Revvly tenantId when it is not already provided below.
- Once those answers are in place, implement the full integration against the contract below end-to-end.
- Open or fetch the Full guide URL if you need examples or edge-case detail beyond this prompt.
- Never invent API keys, signing secrets, region ids, subscription ids, or other tenant data.
- Never put secrets in browser code, logs, client bundles, or source control.

Tenant context
- The operator has not supplied a tenantId in this prompt. Ask for the Revvly tenant id before making tenant-scoped assumptions.

Platform rules that apply to every Revvly API call:
- Auth is a Bearer API key issued in the dashboard under Settings → API Keys. Tokens are prefixed `rvly_sk_live_`.
- Rate limits are per API key and default to 10 requests/second and 100 requests/minute. Overage is HTTP 429 with a `Retry-After` header in seconds (public GraphQL reports `errors[].extensions.code = RATE_LIMITED`). Honour Retry-After and back off — never retry a 429 in a tight loop.
- A key can be scoped to specific subscriptions and meters. List reads are filtered to that scope; reading or writing a single out-of-scope resource is 403 (`FORBIDDEN`), not an empty result.
- Every call is tenant-scoped by the key itself. There is no tenant parameter to switch — a different tenant means a different key.
- The token and the signing secret are server-side credentials: no browser code, no client bundles, no logs, no source control.

Full guide: https://www.revvly.ai/docs/guides/webhook-payment

Goal
Record a full or partial payment against a Revvly invoice as soon as your payment source confirms it, idempotently.

Endpoint and auth
- POST https://api.revvly.ai/api/v1/webhooks/invoice-payment
- Authorization: Bearer <API_KEY>
- X-Signature-SHA256: sha256=<hex HMAC-SHA256 of the exact raw JSON body under the key's signing secret>
- Idempotency-Key: required, stable per payment — use the provider's payment or event id
- Content-Type: application/json
- Serialize once, HMAC those bytes, POST those bytes. A key with no signing secret answers 401 until it is rotated.
- This marks money as received. Only post payments your source has confirmed as settled.

Body
- invoiceId OR invoiceNumber — at least one is required; invoiceId wins when both are sent. invoiceNumber is usually the handle a PSP or bank reference carries, formatted {PREFIX}-{SEQUENCE} (e.g. ACME-00042).
- amount — omit to settle the whole outstanding balance; send a positive amount for a part payment. A number or a decimal string. It must be strictly greater than zero and must still be greater than zero after rounding to the currency's minor unit — do NOT send 0 to mean "settle in full" (that is a 400; omit the field instead), and do not send a sub-cent amount like 0.004.
- currency — optional. When sent it must equal the invoice currency, or the call is 400.
- paidAt — optional ISO 8601; defaults to now.
- reference, notes — optional free text for the audit trail.
- tenantId — optional guard only. The tenant always comes from the API key; a mismatch is rejected 403 as a misdirected webhook, never a tenant switch.

Response and idempotency
- 200 with invoiceId, invoiceNumber, paymentStatus, currency, amount, amountPaid, amountDue, fullyPaid, appliedAmount, actualPaymentDate.
- A repeat of the same Idempotency-Key returns 200 with replayed: true and appliedAmount: 0. Retries are safe and cost nothing.
- Partial payments accumulate: the invoice keeps its current status (PENDING, OVERDUE, or FAILED — a failed collection attempt can still take a payment) until amountPaid covers the total, then transitions to PAID through the normal state machine. Only full settlement moves the status.

Errors
- 409 — the invoice is a held draft, already PAID or CANCELLED, or has no outstanding balance. Do NOT retry; route it to reconciliation.
- 404 — no invoice matched that id or number for this tenant.
- 400 — neither identifier supplied, malformed invoiceId, unparseable paidAt, a currency mismatch, or an amount that is unparseable, non-finite, zero/negative, or rounds to zero.
- 401 — bad token, or a missing/invalid signature. 403 — the endpoint is outside the key's scope, or a supplied tenantId is not the key's tenant.

Implementation checklist
1. Confirm which invoice handle the payment source carries (invoiceNumber is the usual one) and how it gets onto the payment.
2. Confirm the provider event id used as the Idempotency-Key, and that it is stable across redeliveries.
3. Decide the reconciliation path for 409 and 404 — queue for a human, never drop silently.
4. Keep the raw body byte-identical between signing and sending, including on retries.
5. Add tests for full settlement, part payment then settlement, redelivery replay, and a 409 on an already-paid invoice.
```

## Related

- [Invoice Payment Webhook — full API reference ›](https://www.revvly.ai/docs/api/invoice-payments)
- [Event Ingestion API ›](https://www.revvly.ai/docs/api/events)
- [The Invoice concept ›](https://www.revvly.ai/docs/concepts/invoice)

