Agent skill
Invoice Ninja Webhooks Skill
Receive and authenticate Invoice Ninja webhooks. Use when setting up an Invoice Ninja webhook endpoint, checking the secret header you add to the webhook, debugging 401s or a webhook subscription that disappeared, or handling entity payloads for create_invoice, create_payment and update_client.
Install this skill
npx skills add hookdeck/webhook-skills --skill invoice-ninja-webhooks
Invoice Ninja sends an HTTP request to your URL when an entity (client, invoice, quote, credit, payment, vendor, expense, task, project, product, purchase order) is created, updated, sent, archived, restored or deleted. Webhooks are not signed. You authenticate them with a secret header that you add to the webhook subscription (Headers key/value) and check on your endpoint.
When to Use This Skill
- How do I receive Invoice Ninja webhooks?
- How do I verify an Invoice Ninja webhook? (There is no signature: use a secret header)
- How do I handle
create_invoice,create_paymentorupdate_clientwebhooks? - Why did my Invoice Ninja webhook subscription get archived?
- Why does my endpoint return 401 for Invoice Ninja requests?
Verification (core)
const crypto = require('crypto');
// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
const ha = crypto.createHash('sha256').update(String(a)).digest();
const hb = crypto.createHash('sha256').update(String(b)).digest();
return crypto.timingSafeEqual(ha, hb);
}
function verifyInvoiceNinjaRequest(
headers, // Node lower-cases header names
secret = process.env.INVOICE_NINJA_WEBHOOK_SECRET, // exact, whole header value
headerName = process.env.INVOICE_NINJA_WEBHOOK_HEADER || 'Authorization'
) {
if (!secret) return false; // never accept when unconfigured
const received = headers[headerName.toLowerCase()];
return Boolean(received) && safeEqual(received, secret);
}
- The header name is yours: it is whatever key you typed in the webhook's Headers. Read it from
INVOICE_(defaultNINJA_ WEBHOOK_ HEADER Authorization) and look it up case-insensitively. - Compare the whole value with
INVOICE_, including anyNINJA_ WEBHOOK_ SECRET Bearerprefix you typed. Do not strip or parse the prefix. - Python:
hmac.compare_(bytes, neverdigest(received.encode(), secret.encode()) str, which raises on non-ASCII). - Missing header or mismatch:
401. Bad body:400. Handled:200. - Read the body raw (
express.raw({ type: '*/*' })) and check the header before parsing it.
A static shared credential authenticates the sender, not the body. There is no signature and no timestamp, so it gives no replay protection and does not prove the body is unmodified. Use HTTPS only and rotate the header value in Invoice Ninja (and your env var) if it leaks.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Never Answer 404, 405 or 410
If your endpoint responds 404, 405 or 410, Invoice Ninja treats the URL as gone and soft-deletes the webhook subscription, the same state as archiving, so it stops sending until you restore it. Use 401 for bad credentials and 400 for bad bodies. Check that your framework or proxy doesn't return one of these for an unexpected method or path. Other 4xx and 5xx responses are logged and not retried (only connection errors and 429 are retried, up to 5 tries), so a wrong secret loses events instead of delaying them.
Request Method and Body
- The method is chosen on the subscription (
rest_method). Invoice Ninja's server only acceptspost(the default) orputthere, so acceptPOSTandPUTon the route. Hookdeck's Invoice Ninja source pre-selectsPOSTwithout managing the method, so allowPUTon the source too if you use it. - The body is JSON: the entity as produced by Invoice Ninja's transformer, with no wrapper and no event field.
Routing Events
The event key names the subscription only; it is not sent in the request. Route by URL (one subscription per event, for example https://example.com/webhooks/invoice-ninja?event=create_) or by entity shape (line_items for documents, applied for payments, contacts plus paid_to_date for clients, because vendors have contacts too). The examples support both.
Event Keys
Exact keys, as listed in Invoice Ninja's Webhook model. Each is <action>_<entity>.
| Key | Fires When |
|---|---|
create_invoice | An invoice is created |
create_payment | A payment is created |
update_client | A client is updated |
sent_invoice, late_invoice, remind_invoice | An invoice is sent, goes late, or a reminder is sent |
create_quote, approve_quote, expired_quote | Quote lifecycle |
create_client, create_credit, create_expense, create_task, create_project, create_vendor, create_product, create_purchase_order | Entity created |
The same update_, archive_, restore_ and delete_ actions exist for most entities. See references/overview.md for the full list.
Environment Variables
INVOICE_NINJA_WEBHOOK_HEADER=Authorization # header key you added to the webhook
INVOICE_NINJA_WEBHOOK_SECRET=your-secret-value # the exact, whole header value
Local Development
Invoice Ninja can't reach localhost. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 invoice-ninja --path /webhooks/invoice-ninja
Use the printed URL as the webhook Target URL in Invoice Ninja.
Resources
- references/overview.md - What Invoice Ninja webhooks are, event keys, payload shape
- references/setup.md - Create the webhook and add the secret header
- references/verification.md - Header check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests