Lunch
Guides

Webhooks

Advance and payment events occur after your request returns. Subscribe to receive them instead of polling.

Subscribing

TerminalCode
curl -X POST https://api.lunchpayments.com/v1/webhooks \ -H "Authorization: Bearer lux_sk_..." \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/lunch/webhooks" }'

Omit eventTypes to receive all events, including future ones.

Store the signing secret immediately

The signing secret is returned only in this response. If the response is lost, see Idempotent Requests before retrying.

Technical Details

RequirementDetail
EndpointPublic https URL.
Refused addressesPrivate, loopback and link-local.
RedirectsNot followed.
SuccessAny 2xx response.
RetriesOther responses are retried for about 28 hours.
Retired endpointsReturn 410 to stop deliveries and revoke the subscription.

Authentication

Each delivery includes Lunch-Signature: t=<unix seconds>,v1=<hex>: an HMAC-SHA256 of "<timestamp>.<raw body>", keyed with your signing secret.

JavascriptCode
import { createHmac, timingSafeEqual } from 'node:crypto'; // `raw` is the unparsed request body. Parsing and re-serialising it will not verify. // Anything malformed answers false rather than throwing: this runs on an endpoint the whole // internet can reach, and a header somebody made up should not become a 500. export const verify = (raw, header, secret) => { if (typeof header !== 'string') return false; const parts = Object.fromEntries( header.split(',').map((part) => { const at = part.indexOf('='); return at === -1 ? ['', ''] : [part.slice(0, at).trim(), part.slice(at + 1).trim()]; }), ); if (!parts.t || !parts.v1 || !/^[0-9a-f]+$/i.test(parts.v1)) return false; const age = Math.abs(Date.now() / 1000 - Number(parts.t)); if (!Number.isFinite(age) || age > 300) return false; const expected = createHmac('sha256', secret).update(`${parts.t}.${raw}`).digest(); const given = Buffer.from(parts.v1, 'hex'); return expected.length === given.length && timingSafeEqual(expected, given); };

Check the timestamp

Reject deliveries older than five minutes. A valid signature does not prove a delivery is recent.

Webhook Idempotency

Delivery is at-least-once. Deduplicate on Lunch-Delivery, which is constant across retries.

Ordering

Delivery order is not guaranteed, and out-of-order deliveries are not corrected. A retried delivery can arrive after a later one.

  • partner.invoice.factoredUpdated includes a sequence. Ignore events whose sequence is not greater than the last one processed for that invoice.
  • Do not order by occurredAt. It is the transaction start time. Use it for logging.

Events

Event names use loan for advances.

EventDescription
partner.organization.addedAn organization you synced exists at Lunch.
partner.organization.optedInThe organization has completed onboarding.
partner.organization.remittanceUpdatedThe destination for the organization's funds has changed.
partner.invoice.createdAn invoice you synced exists at Lunch.
partner.invoice.paidThe payor has paid the invoice.
partner.invoice.factoredUpdatedThe advance's financing state has changed: REQUESTED, FUNDING, FUNDED or REPAID.
partner.loan.createdAn advance has been created.
partner.loan.issuedThe vendor has been paid the advance.
partner.loan.paidThe advance has settled.

Managing Subscriptions

ActionCall
List active subscriptions (secrets excluded)GET /v1/webhooks
Delete a subscriptionDELETE /v1/webhooks/{reference}

What's Next

Last modified on