Webhooks & Events

Storno.ro delivers real-time updates through three complementary channels: outbound HTTP webhooks for server-to-server integrations, WebSocket connections (Centrifugo) for live UI updates, and in-app notifications for user-facing alerts.


Outbound Webhooks

Outbound webhooks send HTTP POST requests to your server whenever business events occur. Use them to integrate Storno.ro with ERPs, accounting tools, Slack, or any system that accepts HTTP callbacks.

How it works

  1. You create a webhook endpoint with a destination URL and the events you want to receive
  2. Storno.ro generates an HMAC-SHA256 signing secret (shown only once — store it securely)
  3. When a subscribed event occurs, Storno.ro sends a signed POST request to your URL
  4. Your server verifies the signature and processes the payload
  5. If delivery fails, Storno.ro retries up to 3 times with exponential backoff

Webhook payload format

Every webhook delivery sends a JSON payload with this structure:

{
  "id": "0192b3a4-5c6d-7e8f-9a0b-1c2d3e4f5a6b",
  "event": "invoice.validated",
  "created_at": "2026-02-19T10:30:00+00:00",
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "number": "UEP2026000002",
    "status": "validated",
    "direction": "outgoing",
    "total": "30940.00",
    "currency": "RON"
  }
}
FieldTypeDescription
idstringUnique delivery ID (UUID v7)
eventstringEvent type that triggered the webhook
created_atstringISO 8601 timestamp of when the event occurred
dataobjectEvent-specific payload (varies by event type)

HTTP headers

Each delivery includes these headers:

HeaderDescription
Content-Typeapplication/json
X-Webhook-SignatureHMAC-SHA256 hex digest of the raw body, signed with your secret
X-Webhook-EventEvent type name (e.g., invoice.validated)
X-Webhook-IdUnique delivery ID (same as payload id)
User-AgentStorno-Webhook/1.0

Verifying signatures

Always verify the X-Webhook-Signature header to ensure the request came from Storno.ro. Compute the HMAC-SHA256 of the raw request body using your signing secret and compare:

const crypto = require('crypto');

function verifyWebhook(rawBody, signatureHeader, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signatureHeader)
  );
}

// Express middleware example
app.post('/webhooks/storno', (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const isValid = verifyWebhook(req.rawBody, signature, process.env.WEBHOOK_SECRET);

  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(req.rawBody);
  console.log(`Received ${event.event}:`, event.data);

  res.status(200).send('OK');
});
⚠️

Always use constant-time comparison (timingSafeEqual, hash_equals, hmac.compare_digest) to prevent timing attacks.

Retry policy

If your endpoint returns a non-2xx status code or the connection fails, Storno.ro retries with exponential backoff:

AttemptDelayTotal elapsed
1Immediate0
21 minute1 min
35 minutes6 min

After 3 failed attempts the delivery is marked as failed and no further retries are attempted. You can monitor delivery status and errors through the delivery log endpoints.

Best practices for your endpoint:

  • Return 200 OK as quickly as possible — process the payload asynchronously
  • Respond within 10 seconds (the request times out after that)
  • Handle duplicate deliveries idempotently using the id field
  • If you return a 2xx status, the delivery is considered successful

Delivery statuses

StatusDescription
pendingDelivery queued, not yet attempted
successYour endpoint returned a 2xx response
retryingDelivery failed, retry scheduled
failedAll retry attempts exhausted

Event Types

Storno.ro supports 15 event types organized into 5 categories. Retrieve the full list from the API via GET /api/v1/webhooks/events.

Invoice events

EventDescriptionPayload fields
invoice.createdNew invoice created or synced from e-invoice providerid, number, status, direction, total, currency
invoice.issuedInvoice issued (draft finalized)id, number, status, direction, total, currency
invoice.validatedE-invoice provider validated an outgoing invoiceid, number, status, direction, total, currency
invoice.rejectedE-invoice provider rejected an outgoing invoiceid, number, status, direction, total, currency
invoice.sent_to_providerInvoice submitted to e-invoice providerid, number, status, direction, total, currency

Company events

EventDescriptionPayload fields
company.createdNew company added to the organizationid, name, cif
company.updatedCompany data modifiedid, name, cif
company.removedCompany soft-deletedid, name, cif
company.restoredCompany restored from soft-deleteid, name, cif
company.resetCompany data reset (invoices, clients cleared)id, name, cif

Sync events

EventDescriptionPayload fields
sync.startedE-invoice sync process startedcompany_id, cif
sync.completedE-invoice sync finished successfullycompany_id, cif, invoices_synced
sync.errorE-invoice sync encountered an errorcompany_id, cif, error

Payment events

EventDescriptionPayload fields
payment.receivedPayment recorded on an invoiceid, invoice_id, amount, currency, payment_method

Provider Authentication Events

EventDescriptionPayload fields
anaf.token_createdNew ANAF OAuth token obtainedcompany_id, cif, expires_at

Webhook Management

Creating a webhook

curl -X POST https://api.storno.ro/api/v1/webhooks \
  -H "Authorization: Bearer {token}" \
  -H "X-Company: {company_uuid}" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.example.com/webhooks",
    "events": ["invoice.created", "invoice.validated", "payment.received"],
    "description": "ERP integration"
  }'
⚠️

The signing secret is returned in full only on creation and on regenerate-secret. Store it securely — all subsequent reads return a masked value.

Testing a webhook

Send a test delivery to verify your endpoint is reachable:

curl -X POST https://api.storno.ro/api/v1/webhooks/{uuid}/test \
  -H "Authorization: Bearer {token}" \
  -H "X-Company: {company_uuid}"

The test sends a webhook.test event synchronously and returns the HTTP result immediately:

{
  "success": true,
  "statusCode": 200,
  "durationMs": 145,
  "error": null
}

Viewing delivery history

curl https://api.storno.ro/api/v1/webhooks/{uuid}/deliveries?page=1&limit=20 \
  -H "Authorization: Bearer {token}" \
  -H "X-Company: {company_uuid}"

Each delivery record includes the event type, HTTP status code, response time, attempt number, and any error message. Use the delivery detail endpoint to inspect the full request payload and response body.

Permissions

PermissionRolesActions
webhook.viewAdmin, AccountantList endpoints, view details, view delivery log
webhook.manageAdminCreate, update, delete, test, regenerate secret

API endpoints

MethodEndpointDescription
GET/api/v1/webhooks/eventsList available event types
GET/api/v1/webhooksList webhook endpoints
POST/api/v1/webhooksCreate a webhook endpoint
GET/api/v1/webhooks/{uuid}Get endpoint details
PATCH/api/v1/webhooks/{uuid}Update endpoint
DELETE/api/v1/webhooks/{uuid}Delete endpoint
POST/api/v1/webhooks/{uuid}/testSend test delivery
POST/api/v1/webhooks/{uuid}/regenerate-secretRegenerate signing secret
GET/api/v1/webhooks/{uuid}/deliveriesList delivery history
GET/api/v1/webhooks/{uuid}/deliveries/{id}Get delivery detail

Real-Time Updates (Centrifugo)

For live UI updates (browser and mobile), Storno.ro uses Centrifugo WebSocket connections. This is separate from outbound webhooks and intended for front-end applications.

Connecting

  1. Obtain a connection token:
curl -X POST https://api.storno.ro/api/v1/centrifugo/connection-token \
  -H "Authorization: Bearer {token}"
  1. Connect to the Centrifugo WebSocket server with the returned token

  2. Subscribe to channels:

curl -X POST https://api.storno.ro/api/v1/centrifugo/subscription-token \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "user:{user_id}"
  }'

WebSocket event types

Events are published to user-specific and company-specific channels:

EventDescription
invoice.createdNew invoice created (including synced from e-invoice provider)
invoice.updatedInvoice status or data changed
invoice.paidPayment recorded on invoice
sync.completedE-invoice sync finished
sync.errorE-invoice sync encountered an error
notification.newNew notification for the user

Notifications

Storno.ro sends user-facing notifications through multiple channels.

Channels

ChannelDescription
in_appIn-app notifications (visible in notification panel)
emailEmail notifications
pushPush notifications (iOS, Android, Web)

Notification types

TypeDescription
invoice.validatedE-invoice provider validated an outgoing invoice
invoice.rejectedE-invoice provider rejected an outgoing invoice
invoice.overdueInvoice past its due date
payment.receivedPayment recorded on an invoice
invoice.issuedInvoice issued
invoice.paidInvoice fully paid

Managing preferences

Users can control which notifications they receive on which channels:

# Get current preferences
curl https://api.storno.ro/api/v1/notification-preferences \
  -H "Authorization: Bearer {token}"

# Update preferences
curl -X PUT https://api.storno.ro/api/v1/notification-preferences \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{
    "preferences": {
      "invoiceValidated": { "email": true, "inApp": true, "push": false },
      "invoiceRejected": { "email": true, "inApp": true, "push": true },
      "paymentReceived": { "email": true, "inApp": true, "push": false }
    }
  }'

Push notifications

To receive push notifications, register a device token:

curl -X POST https://api.storno.ro/api/v1/devices \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{
    "token": "firebase_device_token_here",
    "platform": "android"
  }'

Supported platforms: ios, android, web.


Invoice Events (Audit Log)

Each invoice maintains a history of status changes and significant events:

curl https://api.storno.ro/api/v1/invoices/{uuid}/events \
  -H "Authorization: Bearer {token}" \
  -H "X-Company: {company_uuid}"
[
  {
    "type": "status_change",
    "status": "issued",
    "timestamp": "2026-02-15T10:30:00Z",
    "details": "Invoice issued by [email protected]"
  },
  {
    "type": "status_change",
    "status": "sent_to_provider",
    "timestamp": "2026-02-15T10:31:00Z",
    "details": "Submitted to ANAF (upload ID: 12345)"
  },
  {
    "type": "status_change",
    "status": "validated",
    "timestamp": "2026-02-15T11:00:00Z",
    "details": "Validated by ANAF"
  }
]