Webhooks
Receive events when transactions are enriched, when carbon thresholds are crossed, when budgets tip, or when subscriptions are detected. At-least-once delivery, signed payloads, retries with exponential backoff.
Register an endpoint
Register your HTTPS endpoint and choose which event types you want.
POST /v1/webhooks
{
"url": "https://your-backend.example.com/lune-webhook",
"events": ["transaction.enriched", "budget.exceeded"],
"description": "Production sync"
}
The response includes a signing_secret. Store it — you'll use it to verify every
incoming payload.
Event types
| Event | Fired when |
|---|---|
transaction.enriched | A transaction has been enriched (sync or batch). |
transaction.refund_matched | A new refund was matched to an existing transaction. |
subscription.detected | A recurring charge has been classified as a subscription. |
budget.exceeded | A user's category budget threshold was crossed. |
goal.achieved | A user reached a savings goal target. |
carbon.threshold | A user crossed a monthly carbon-footprint threshold. |
Payload shape
{
"id": "evt_01HXYZABC...",
"type": "transaction.enriched",
"created_at": "2026-05-23T14:02:11Z",
"data": {
"transaction_id": "txn_...",
"merchant": { /* same shape as Enrich response */ },
"category": "food.coffee"
}
}
Verifying signatures
Each request carries an X-Lune-Signature header — an HMAC-SHA256 of the raw body
signed with your endpoint's signing_secret. Always verify before processing.
import crypto from 'node:crypto';
function verify(rawBody, sig, secret) {
const expected = crypto.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(sig),
);
}
import hmac, hashlib
def verify(raw_body: bytes, sig: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, sig)
func verify(rawBody []byte, sig, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(rawBody)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(sig))
}
Retries
If your endpoint returns anything other than a 2xx status (or times out after 10
seconds), Lune retries with exponential backoff: 30s, 2m, 10m, 1h, 6h, 24h, then gives up
and marks the event as failed. Failed events can be replayed from the dashboard or
POST /v1/webhooks/events/:id/retry.
Best practices
- Respond with
200as fast as possible — push processing to a queue. - Always verify the signature before doing anything with the payload.
- Idempotency: store
event.id; ignore duplicates (Lune may deliver twice). - Use the dashboard's "Send test event" button to verify your endpoint locally with a tool like ngrok.
