Skip to Content

Webhooks

Receive real-time notifications when events happen in your Yona account.

Setting up a webhook

bash
curl -X POST https://gateway.useyona.com/a/v1/organizations/9f8e7d6c-5b4a-3210-fedc-ba9876543210/webhooks \
  -H "Authorization: Bearer sk_test_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/yona",
    "events": ["invoice.approved", "invoice.rejected", "invoice.submission_failed"],
    "description": "Production invoice notifications"
  }'

Save the secret from the response — you need it to verify signatures.

Available events

These are the events you can subscribe to:

Invoice events

EventDescription
invoice.createdInvoice created (draft status)
invoice.updatedDraft invoice modified
invoice.submittedInvoice queued for submission to tax authority
invoice.approvedTax authority accepted the invoice
invoice.rejectedTax authority rejected the invoice
invoice.submission_failedSubmission failed before tax authority response
invoice.cancelledInvoice cancelled (credit note created)
invoice.status.changedInvoice status changed via authority callback
invoice.retry_scheduledFailed invoice re-queued for retry

Seller & Buyer events

EventDescription
seller.createdNew seller registered
seller.updatedSeller details changed
seller.tax_number_verifiedSeller TIN verification succeeded
seller.tax_number_verification_failedSeller TIN verification failed
buyer.createdNew buyer registered
buyer.updatedBuyer details changed
buyer.deletedBuyer soft-deleted
buyer.tax_number_verifiedBuyer TIN verification succeeded
buyer.tax_number_verification_failedBuyer TIN verification failed

User events

EventDescription
user.createdUser signed up or accepted invitation
user.updatedUser profile changed
user.deletedUser soft-deleted
user.role_changedUser role reassigned
user.status_changedUser status changed (active/suspended/locked)

Organization events

EventDescription
organization.createdOrganization created
organization.updatedOrganization metadata changed
organization.status_changedOrganization status changed
organization.tax_number_verifiedOrganization TIN verified
organization.onboarding_completedOnboarding workflow completed
organization.subscription_tier_changedSubscription tier changed

API key events

EventDescription
api_key.createdNew API key generated
api_key.revokedAPI key revoked
api_key.rotatedAPI key rotated (new key issued)
api_key.permissions_changedAPI key permissions updated

Payment & billing events

EventDescription
payment.createdPayment initiated
payment.succeededPayment completed
payment.failedPayment declined or failed
billing_account.status_changedBilling account suspended or reactivated
billing_account.low_balanceCredit balance dropped below threshold

Subscription & credit events

EventDescription
subscription.createdNew subscription created
subscription.renewedSubscription renewed
subscription.cancelledSubscription cancelled
subscription.expiredSubscription expired (unpaid)
subscription.plan_changedPlan upgrade/downgrade applied
subscription.payment_failedRenewal payment declined
subscription.payment_failed_finalAll renewal retries exhausted
credit.balance_updatedCredit balance changed (any mutation)
credit.purchasedCredits purchased

Collection events

For the Collection API — collecting invoice payments from buyers via virtual accounts.

EventDescription
collection.payment_receivedA buyer transfer landed on a collection account
collection.payment_reconciledA received payment was matched to an invoice
collection.payment_appliedA payment was applied to invoice(s); carries appliedAmount, unappliedAmount, invoiceIds, and the outcome status (applied/overpaid/unmatched)
collection.settlement_updatedA seller’s settlement account / collection readiness changed

Meta events

EventDescription
webhook.testTest event for endpoint verification
*Wildcard — subscribe to all events

Payload shape

{ "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "type": "invoice.approved", "environment": "sandbox", "timestamp": "2026-04-30T11:10:05.000Z", "data": { "invoiceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "invoiceNumber": "INV-2026-001", "status": "accepted", "jurisdiction": "NG", "authorityReference": "NRS-REF-001", "submittedAt": "2026-04-30T11:10:05.000Z" } }

Signature verification

Every webhook delivery includes these headers:

HeaderDescription
X-Webhook-SignatureHMAC-SHA256 signature: sha256=<hex>
X-Webhook-TimestampUnix timestamp (seconds)
X-Webhook-EventEvent type string
X-Webhook-IDEvent ID (UUID)
X-Webhook-Delivery-AttemptAttempt number (starts at 1)

Always verify the signature before processing.

import express from 'express'; import { verifyWebhook } from '@useyona/einvoice-js/webhooks'; const app = express(); app.post('/webhooks/yona', express.json(), (req, res) => { try { const event = verifyWebhook( req.body, req.headers, process.env.WEBHOOK_SECRET!, ); switch (event.type) { case 'invoice.approved': handleApproved(event.data); break; case 'invoice.rejected': handleRejected(event.data); break; case 'invoice.submission_failed': handleFailed(event.data); break; } res.status(200).json({ received: true }); } catch (err) { res.status(400).json({ error: 'Invalid signature' }); } });
💡 Raw body or parsed JSON — both work

You can pass either the raw body (Buffer/string) or JSON.stringify(req.body) — the SDK handles both. Using express.json() or express.raw() are both fine.

Manual verification

If you prefer not to use the SDK, you can verify signatures manually. The backend signs only the data field — not the entire body.

import express from 'express'; import crypto from 'crypto'; const app = express(); app.post('/webhooks/yona', express.json(), (req, res) => { const signature = req.headers['x-webhook-signature'] as string; const timestamp = req.headers['x-webhook-timestamp'] as string; if (!signature || !timestamp) { return res.status(400).json({ error: 'Missing signature headers' }); } // Replay protection: reject timestamps older than 5 minutes const age = Math.abs(Math.floor(Date.now() / 1000) - parseInt(timestamp)); if (age > 300) { return res.status(400).json({ error: 'Timestamp too old' }); } // Backend signs only the `data` field const signedPayload = `${timestamp}.${JSON.stringify(req.body.data)}`; const expected = `sha256=${crypto .createHmac('sha256', process.env.WEBHOOK_SECRET!) .update(signedPayload) .digest('hex')}`; if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) { return res.status(400).json({ error: 'Invalid signature' }); } // Signature valid — process event const { type, data, environment } = req.body; switch (type) { case 'invoice.approved': handleApproved(data); break; case 'invoice.rejected': handleRejected(data); break; case 'invoice.submission_failed': handleFailed(data); break; } res.status(200).json({ received: true }); });

Retry logic

  • 3 total attempts (1 initial + 2 retries)
  • Backoff: exponential (1s initial delay, 2x multiplier)
  • Success = HTTP 2xx response
  • Timeout = 10 seconds per delivery attempt
  • Endpoints can override with a custom retryConfig
  • After all attempts fail, the delivery is marked as failed

You can manually retry a failed delivery:

bash
curl -X POST https://gateway.useyona.com/a/v1/organizations/9f8e7d6c-5b4a-3210-fedc-ba9876543210/webhooks/deliveries/{delivery_id}/retry \
  -H "Authorization: Bearer sk_test_your_key_here"

Testing webhooks

Send a test event to verify your endpoint:

bash
curl -X POST https://gateway.useyona.com/a/v1/organizations/9f8e7d6c-5b4a-3210-fedc-ba9876543210/webhooks/{webhook_id}/test \
  -H "Authorization: Bearer sk_test_your_key_here"

In sandbox mode, all webhook events fire normally. You can use tools like webhook.site  for testing.

💡 Full API reference

See API Reference — Webhooks for the complete endpoint list.

Last updated on