Webhooks
When a monitored taxpayer changes in the FURS lists, we POST a signed event to your URL following Standard Webhooks. Starter plan and up, key scope webhooks. Deliveries cost no units.
curl -X POST https://api.maticna.si/v1/webhook-endpoints -H "Authorization: Bearer $MATICNA_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://erp.example.com/hooks/maticna", "event_types": ["deactivated", "vat_deregistered", "vat_revoked"]}'The response contains secret (whsec_…) only once; we store it encrypted and later show only secret_hint. URLs must be public https:// addresses (checked again after DNS resolution on every delivery; 422 invalid_webhook_url). Up to 10 endpoints per org. GET/PATCH/DELETE /v1/webhook-endpoints/{id}; POST /v1/webhook-endpoints/{id}/test sends a signed webhook.test within about a minute; POST /v1/webhook-endpoints/{id}/roll-secret issues a new secret (rolling the secret).
The body is {"type": "entity.<change_type>", "timestamp": "…", "data": <change object>} (see changes), with headers webhook-id, webhook-timestamp and webhook-signature: v1,<base64>.
Verifying the signature (Node.js)
Section titled “Verifying the signature (Node.js)”HMAC-SHA256 over {webhook-id}.{webhook-timestamp}.{raw body}, keyed with the base64 part of the secret after whsec_. Verify the raw body and reject timestamps older than 5 minutes:
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyMaticnaWebhook(secret, headers, rawBody) { const id = headers['webhook-id']; const ts = headers['webhook-timestamp']; const signatures = headers['webhook-signature']; if (!id || !ts || !signatures) return false; if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; const key = Buffer.from(secret.slice('whsec_'.length), 'base64'); const expected = createHmac('sha256', key).update(`${id}.${ts}.${rawBody}`).digest(); return signatures.split(' ').some((s) => { const [version, sig] = s.split(','); const got = Buffer.from(sig ?? '', 'base64'); return version === 'v1' && got.length === expected.length && timingSafeEqual(got, expected); });}The official Standard Webhooks libraries verify the same signature.
Rolling the secret
Section titled “Rolling the secret”curl -X POST https://api.maticna.si/v1/webhook-endpoints/$ID/roll-secret -H "Authorization: Bearer $MATICNA_KEY" \ -H "Content-Type: application/json" -d '{"grace_period_hours": 24}'The response contains the new secret (again only once) and previous_secret_expires_at. The new secret applies at once, retries included. The old one stays valid for grace_period_hours (default 24, at most 168): meanwhile every event carries two signatures, webhook-signature: v1,<new> v1,<old>. The function above and the Standard Webhooks libraries accept an event when any signature matches, so your server keeps working with either secret during the switch. Deploy the new secret before the grace period ends; after it, events are signed with the new secret only.
grace_period_hours: 0retires the old secret immediately (e.g. after a leak).- Rolling again during a grace period retires the older secret at once; at most two secrets are valid.
- Without a body the default of 24 hours applies. The dashboard’s “Roll secret” button uses 24 hours as well.
Delivery and retries
Section titled “Delivery and retries”- Success is any
2xxwithin 10 seconds. Redirects are not followed and count as failures. - Failed deliveries are retried after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h and 24 h (±10 %), about 3 days in total, always with the same
webhook-id(deduplicate on it). Order is not guaranteed; sort bydata.seq. - After 20 consecutive failed attempts the endpoint is disabled (
disabled_reason: consecutive_failures). Re-enable it withPATCH {"enabled": true}; events from the disabled period are not replayed. - Webhooks are a Starter-and-up feature. If the org moves to a plan without webhooks, its endpoints are disabled (
enabled: false,disabled_reason: plan_downgraded), not deleted. Pending deliveries end as failed (plan does not include webhooks: delivery withdrawn) and are never sent later. After an upgrade the endpoints are enabled again automatically; events from the time in between are not replayed. An endpoint you disabled yourself meanwhile (PATCH {"enabled": false}) stays disabled.
Source: FURS · unofficial source, not an official extract · Terms · Privacy · maticna.si