Source: https://www.revvly.ai/docs/api/invoice-payments

API Reference

# Mark an invoice *paid* from your payment system.

Post a payment to Revvly the moment your processor, bank feed, or accounting sync sees the money land — in full, or a part payment that leaves the rest outstanding. No one has to open the dashboard to keep receivables current.

## Authentication

The same two credentials as the [Event Ingestion API](https://www.revvly.ai/docs/api/events), issued together when you create or rotate an API key. Both are shown once and never again.

- **Bearer token**: Sent in the Authorization header. Identifies your workspace — the payment is applied to that workspace and no other. The key must hold PAYMENT_WRITE (“Can record payments” when you issue it).

- **Signing secret**: Used to compute the X-Signature-SHA256 HMAC over the raw request body. A leaked Bearer token on its own cannot mark anything paid.

Request headers

```
Authorization:      Bearer rvly_sk_live_<your-token>
Content-Type:       application/json
X-Signature-SHA256: sha256=<hmac-hex>
Idempotency-Key:    <your-event-id>
```

i

Signing works exactly as it does for usage events — HMAC-SHA256 over the **raw body bytes**, keyed with the signing secret, sent as sha256=<hex>. Re-serializing the JSON after signing invalidates it. The [Event Ingestion page](https://www.revvly.ai/docs/api/events) covers the mechanics in full.

i

Keys issued before signing secrets existed do not have one. Rotate the key from **Settings → API Keys** to provision it. If your key is scoped to specific endpoints, include /webhooks/invoice-payment (or /webhooks/*) in that scope.

## [#](#post-invoice-payment)Endpoint

POST

https://api.revvly.ai/api/v1/webhooks/invoice-payment

200 OK

One payment per request, against one invoice. Omit amount and Revvly settles whatever is still outstanding; send an amount and it is recorded as a part payment.

## Request body

- **invoiceNumberone of**: The number printed on the invoice — e.g. ACME-00042. Usually the reference your processor or bank feed already carries, so you never have to store a Revvly id. Supply **either** this **or** invoiceId.

- **invoiceIdone of**: The internal Revvly invoice id. Use it when you already sync Revvly ids; otherwise prefer invoiceNumber.

- **amountoptional**: Amount collected, in the invoice’s currency. **Omit it to pay the invoice in full** — Revvly applies the outstanding balance, whatever it is at that moment. Must be greater than zero.

- **currencyoptional**: A safety check, not a conversion: if supplied it must match the invoice’s currency, otherwise the request is rejected. Revvly does not convert a payment made in another currency.

- **paidAtoptional**: ISO 8601 timestamp for when the money actually moved. Defaults to now. Send the real date when you are catching up on a payment received earlier — it is what the invoice is settled on, so your collection figures land in the right period.

- **referenceoptional**: Your payment reference — charge id, bank transaction id. Stored on the payment record for reconciliation.

- **notesoptional**: Free text kept alongside the payment — “wire, part payment”, “settled by credit note”.

- **tenantIdoptional**: A guard, not a selector. Your workspace always comes from the API key; if you send a tenantId that disagrees with the key, the request is refused rather than applied somewhere else. Useful as a tripwire when the same integration is wired to more than one workspace.

i

**The Idempotency-Key header is required.** Use your own event id — the processor’s event id is ideal. Payment senders retry, and this is what stops a retry being counted as a second payment.

## Full examples

A complete request — authentication, signature, idempotency key, and a part payment of 400 against a 1 000 invoice.

```
const crypto = require('crypto');
const https  = require('https');

const API_KEY     = 'rvly_sk_live_<your-token>';
const SIGN_SECRET = '<your-signing-secret>';
const HOST        = 'api.revvly.ai';

// Omit "amount" to settle the invoice in full.
const payload = JSON.stringify({
  invoiceNumber: 'ACME-00042',
  amount:        400,
  currency:      'USD',
  paidAt:        new Date().toISOString(),
  reference:     'pi_3PqR8s2eZvKYlo2C',
});

const sig = crypto
  .createHmac('sha256', SIGN_SECRET)
  .update(payload) // raw bytes — do not re-serialize
  .digest('hex');

const req = https.request({
  host:   HOST,
  path:   '/api/v1/webhooks/invoice-payment',
  method: 'POST',
  headers: {
    'Content-Type':       'application/json',
    'Content-Length':     Buffer.byteLength(payload),
    'Authorization':      `Bearer ${API_KEY}`,
    'X-Signature-SHA256': `sha256=${sig}`,
    // Your own event id — makes the call safe to retry.
    'Idempotency-Key':    'evt_3PqR8s2eZvKYlo2C',
  },
}, (res) => {
  let data = '';
  res.on('data', (chunk) => { data += chunk; });
  res.on('end', () => console.log(JSON.parse(data)));
});

req.write(payload);
req.end();
```

## Response

The invoice as it stands after the payment, so you can reconcile without a follow-up read. amountDue is what is still owed; appliedAmount is what this particular call moved.

Part payment — invoice stays open

```
HTTP/1.1 200 OK

{
  "invoiceId":         "663b1f2e4a3c2d1e0f8b7a9c",
  "invoiceNumber":     "ACME-00042",
  "paymentStatus":     "PENDING",
  "currency":          "USD",
  "amount":            1000,
  "amountPaid":        400,
  "amountDue":         600,
  "fullyPaid":         false,
  "appliedAmount":     400,
  "replayed":          false,
  "actualPaymentDate": null
}
```

The payment that clears the balance — invoice settles

```
HTTP/1.1 200 OK

{
  "invoiceId":         "663b1f2e4a3c2d1e0f8b7a9c",
  "invoiceNumber":     "ACME-00042",
  "paymentStatus":     "PAID",
  "currency":          "USD",
  "amount":            1000,
  "amountPaid":        1000,
  "amountDue":         0,
  "fullyPaid":         true,
  "appliedAmount":     600,
  "replayed":          false,
  "actualPaymentDate": "2026-07-15T10:30:00.000Z"
}
```

### Status codes

- **200**: Payment applied — or the call repeated an Idempotency-Key already seen, in which case replayed is true and appliedAmount is 0.

- **400**: Malformed request — no invoice reference, an amount that is not a positive number, an unparseable paidAt, a currency that is not the invoice’s, or a missing Idempotency-Key.

- **401**: Bad or missing Bearer token, a signature that does not verify, or a key with no signing secret.

- **403**: The key lacks PAYMENT_WRITE, the endpoint is outside the key’s scope, or the tenantId in the body does not match the key.

- **404**: No such invoice in your workspace. Invoices are never visible across workspaces, so a wrong key reads as a missing invoice.

- **409**: The invoice is already paid or cancelled, or has nothing left outstanding.

- **429**: Per-key rate limit reached. Retry after the number of seconds in the Retry-After header.

## Part payments

Each payment is added to the invoice and amountPaid accumulates. Until it covers the total, the invoice stays **Pending** or **Overdue** — it keeps appearing in receivables and keeps drawing payment reminders, which is what you want while money is still owed. The payment that clears the balance settles the invoice to **Paid**, exactly as marking it paid in the dashboard does.

- **Instalments**: Send one call per instalment. Each carries its own Idempotency-Key, paidAt, and reference, and all of them stay on the invoice as a record of how it was collected.

- **Settling the rest**: Omit amount on the last call and Revvly applies whatever is left — you do not have to work out the remainder.

- **Overpayment**: An amount larger than the balance is recorded in full and the invoice settles. amountDue does not go negative.

- **Credit notes**: An invoice with a negative total has nothing to collect, so it is refused with a 409 rather than taking a payment.

⚠

Receivables analytics — AR aging, the cash-flow chart, past-due totals — currently count an open invoice at its **full** amount rather than its outstanding balance, so a part-paid invoice reads high on those dashboards. The invoice itself, and this endpoint, always report the true amountPaid and amountDue.

## Retries and idempotency

Safe to retry. Every payment is recorded under the Idempotency-Key you sent, and a key already on the invoice is never applied a second time — including two copies of the same delivery arriving at once. A replay returns 200 with the invoice’s current state, not an error, so your sender sees success and stops retrying.

applied

replayed: false — appliedAmount was added to the invoice.

replayed

replayed: true — this key was already recorded. Nothing was applied; the totals shown are the invoice as it already stood.

refused

A 409 means the invoice cannot take a payment — already paid, cancelled, or nothing outstanding. Retrying will not change that.

i

Use a key that is stable across the retries of one payment and different between two genuine payments. Your processor’s event id does both; a timestamp or a random value per attempt does neither.

