Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.borga.is/llms.txt

Use this file to discover all available pages before exploring further.

Webhooks let your application react to events happening in Borga without polling the API. When something significant occurs — a payment succeeds, a subscription is canceled, an invoice is paid — Borga sends an HTTP POST request to a URL you control. Your server receives the event, verifies its authenticity using the HMAC signature, and takes whatever action your application requires.
1

Create a webhook endpoint

Register a publicly reachable HTTPS URL with Borga and specify which event types you want to receive.
curl --request POST \
  --url https://api.borga.is/v1/webhook_endpoints \
  --header "Authorization: Bearer sk_test_YOUR_SECRET_KEY" \
  --header "X-Merchant-Id: mer_YOUR_MERCHANT_ID" \
  --header "Content-Type: application/json" \
  --data '{
    "url": "https://yoursite.is/webhooks/borga",
    "events": [
      "payment.succeeded",
      "payment.failed",
      "invoice.paid",
      "subscription.created",
      "subscription.canceled"
    ],
    "description": "Production webhook handler"
  }'
The response includes a secret (prefixed whsec_…). Store this secret securely — you will use it to verify the signature on every incoming delivery. Borga only shows the secret once at creation time.
Webhook endpoints must be reachable over HTTPS. Borga will not deliver events to plain HTTP URLs or to localhost. During development, use a tunnel such as ngrok or a similar tool to expose a local server.
2

Verify webhook signatures

Borga signs every delivery with an HMAC-SHA256 signature computed from the raw request body and a timestamp. Verify the signature before processing any event to ensure the delivery genuinely came from Borga.Each delivery includes two headers:
HeaderDescription
Borga-TimestampUnix timestamp (seconds) when Borga sent the delivery.
Borga-SignatureHMAC-SHA256 of timestamp + "." + rawBody, hex-encoded.
Compute the expected signature and compare it to the one in the header:
Node.js
const crypto = require('crypto');

function verifyBorgaSignature(rawBody, timestamp, signature, secret) {
  const payload = `${timestamp}.${rawBody}`;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload, 'utf8')
    .digest('hex');

  // Use timingSafeEqual to prevent timing attacks
  const expectedBuf = Buffer.from(expected, 'hex');
  const receivedBuf = Buffer.from(signature, 'hex');

  if (expectedBuf.length !== receivedBuf.length) return false;
  return crypto.timingSafeEqual(expectedBuf, receivedBuf);
}
Also check that the timestamp is recent (within 5 minutes) to protect against replay attacks:
Node.js
const TOLERANCE_SECONDS = 300; // 5 minutes

function isTimestampFresh(timestamp) {
  const now = Math.floor(Date.now() / 1000);
  return Math.abs(now - parseInt(timestamp, 10)) <= TOLERANCE_SECONDS;
}
3

Handle events

Build a route in your application to receive deliveries, verify the signature, and dispatch on the event type.
Node.js (Express)
const express = require('express');
const crypto = require('crypto');

const app = express();
const WEBHOOK_SECRET = process.env.BORGA_WEBHOOK_SECRET;

// Use raw body parser so you can verify the signature
app.post('/webhooks/borga', express.raw({ type: 'application/json' }), (req, res) => {
  const timestamp = req.headers['borga-timestamp'];
  const signature = req.headers['borga-signature'];
  const rawBody = req.body.toString('utf8');

  // 1. Verify timestamp freshness
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp, 10)) > 300) {
    return res.status(400).send('Timestamp too old');
  }

  // 2. Verify signature
  const payload = `${timestamp}.${rawBody}`;
  const expected = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(payload, 'utf8')
    .digest('hex');

  const expectedBuf = Buffer.from(expected, 'hex');
  const receivedBuf = Buffer.from(signature, 'hex');

  if (
    expectedBuf.length !== receivedBuf.length ||
    !crypto.timingSafeEqual(expectedBuf, receivedBuf)
  ) {
    return res.status(400).send('Invalid signature');
  }

  // 3. Parse and handle the event
  const event = JSON.parse(rawBody);

  switch (event.type) {
    case 'payment.succeeded':
      // Fulfil the order associated with event.data.id
      console.log('Payment succeeded:', event.data.id);
      break;
    case 'payment.failed':
      // Notify the customer or retry logic
      console.log('Payment failed:', event.data.id);
      break;
    case 'invoice.paid':
      // Update subscription status in your database
      console.log('Invoice paid:', event.data.id);
      break;
    case 'subscription.created':
      console.log('Subscription created:', event.data.id);
      break;
    case 'subscription.canceled':
      // Revoke access at event.data.current_period_end
      console.log('Subscription canceled:', event.data.id);
      break;
    default:
      console.log('Unhandled event type:', event.type);
  }

  // 4. Acknowledge receipt
  res.sendStatus(200);
});
Always return a 2xx response immediately after verifying the signature, even if your processing logic fails or is queued asynchronously. If Borga does not receive a 2xx within 30 seconds, it marks the delivery as failed and will retry. Returning a non-2xx status will trigger unnecessary retries and may cause duplicate processing.
4

Retry failed deliveries

If your endpoint is down or returns a non-2xx response, Borga marks the delivery as failed. You can trigger a manual retry from the API once your endpoint is back online.
curl
curl --request POST \
  --url https://api.borga.is/v1/webhook_deliveries/del_01HXYZ/retry \
  --header "Authorization: Bearer sk_test_YOUR_SECRET_KEY" \
  --header "X-Merchant-Id: mer_YOUR_MERCHANT_ID"
Borga also retries failed deliveries automatically on an exponential backoff schedule. Check the delivery log to see the current status.
5

List delivery history

Inspect past deliveries for a specific endpoint to debug failures or confirm that events were received.
curl
curl --request GET \
  --url "https://api.borga.is/v1/webhook_deliveries?endpoint=we_01HXYZ&limit=20" \
  --header "Authorization: Bearer sk_test_YOUR_SECRET_KEY" \
  --header "X-Merchant-Id: mer_YOUR_MERCHANT_ID"
Each delivery record includes the event type, the HTTP response code your endpoint returned, and the response latency. Use this to diagnose endpoints that are timing out or returning errors.

Event reference

The following events are available to subscribe to:
EventWhen it fires
payment.succeededA payment completed successfully and a charge was captured.
payment.failedA payment attempt was declined or encountered an error.
invoice.paidAn invoice was marked paid, either by automatic charge or manual payment.
subscription.createdA new subscription was created.
subscription.canceledA subscription was canceled, either immediately or at period end.

Event object shape

Every delivery is a POST with a JSON body in this shape:
{
  "id": "evt_01HXYZ1234ABCDEF",
  "type": "payment.succeeded",
  "created_at": "2026-04-29T10:45:00Z",
  "data": {
    "id": "pay_01HXYZ1234ABCDEF",
    "status": "succeeded",
    "amount": 5990,
    "currency": "ISK",
    "customer": "cus_01HXYZ",
    "description": "Order #1234"
  }
}
The data object is the full resource that triggered the event.

Manage existing endpoints

You can update or delete webhook endpoints at any time:
List endpoints
curl --request GET \
  --url https://api.borga.is/v1/webhook_endpoints \
  --header "Authorization: Bearer sk_test_YOUR_SECRET_KEY" \
  --header "X-Merchant-Id: mer_YOUR_MERCHANT_ID"
Update events list
curl --request PATCH \
  --url https://api.borga.is/v1/webhook_endpoints/we_01HXYZ \
  --header "Authorization: Bearer sk_test_YOUR_SECRET_KEY" \
  --header "X-Merchant-Id: mer_YOUR_MERCHANT_ID" \
  --header "Content-Type: application/json" \
  --data '{ "events": ["payment.succeeded", "invoice.paid"] }'
Delete endpoint
curl --request DELETE \
  --url https://api.borga.is/v1/webhook_endpoints/we_01HXYZ \
  --header "Authorization: Bearer sk_test_YOUR_SECRET_KEY" \
  --header "X-Merchant-Id: mer_YOUR_MERCHANT_ID"

Next steps

  • Accept a payment — build the payment flow that generates payment.succeeded events
  • Subscriptions — trigger subscription.created and invoice.paid events with recurring billing
  • Accounting link — automatically sync invoices to your accounting software when invoice.paid fires