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_payment or update_client webhooks?
  • 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_NINJA_WEBHOOK_HEADER (default Authorization) and look it up case-insensitively.
  • Compare the whole value with INVOICE_NINJA_WEBHOOK_SECRET, including any Bearer prefix you typed. Do not strip or parse the prefix.
  • Python: hmac.compare_digest(received.encode(), secret.encode()) (bytes, never 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 accepts post (the default) or put there, so accept POST and PUT on the route. Hookdeck's Invoice Ninja source pre-selects POST without managing the method, so allow PUT on 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_invoice) 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>.

KeyFires When
create_invoiceAn invoice is created
create_paymentA payment is created
update_clientA client is updated
sent_invoice, late_invoice, remind_invoiceAn invoice is sent, goes late, or a reminder is sent
create_quote, approve_quote, expired_quoteQuote lifecycle
create_client, create_credit, create_expense, create_task, create_project, create_vendor, create_product, create_purchase_orderEntity 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 5, 2026

View on GitHub →