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, 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.
iSigning 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 covers the mechanics in full.
iKeys 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.
POSThttps://api.revvly.ai/api/v1/webhooks/invoice-payment200 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 orinvoiceId.
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.
iThe 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.
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.
appliedreplayed: false — appliedAmount was added to the invoice.
replayedreplayed: true — this key was already recorded. Nothing was applied; the totals shown are the invoice as it already stood.
refusedA 409 means the invoice cannot take a payment — already paid, cancelled, or nothing outstanding. Retrying will not change that.
iUse 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.