Webhooks
Webhooks let your application receive real-time HTTP callbacks when events occur in your Gäld organisation. When an event fires, Gäld sends a POST request to each registered webhook URL.
Setting Up a Webhook
Create a webhook via the API:
curl -X POST https://app.gaeld.ch/api/v1/webhooks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/gaeld",
"events": ["invoice.created", "invoice.finalized", "customer.created"],
"is_active": true
}'
Response includes a secret — store it securely, you will need it to verify signatures.
Events
| Event | Fires when |
|---|---|
invoice.created | A new invoice is created |
invoice.updated | An invoice is modified |
invoice.deleted | An invoice is deleted |
invoice.finalized | An invoice is finalized (locked for editing) |
invoice.payment_recorded | A payment is recorded against an invoice |
customer.created | A new customer is created |
customer.updated | A customer is modified |
customer.deleted | A customer is deleted |
expense.created | A new expense is created |
expense.updated | An expense is modified |
expense.deleted | An expense is deleted |
expense.approved | An expense is approved in the workflow |
Use GET /api/v1/meta/webhook-events to retrieve this list programmatically.
Payload Format
Every webhook delivery sends a JSON POST request:
{
"event": "invoice.finalized",
"timestamp": "2025-06-01T12:00:00Z",
"data": {
"id": "uuid",
"number": "INV-2025-042",
"status": "finalized",
"customer": { "id": "uuid", "name": "ACME GmbH" },
"total": "1081.00",
"currency": "CHF"
}
}
The data field contains the full resource representation (same format as the corresponding API endpoint response).
Request Headers
Each webhook request includes these headers:
| Header | Description |
|---|---|
Content-Type | application/json |
X-Webhook-Signature | HMAC-SHA256 hex digest of the request body |
X-Webhook-Event | Event name (e.g. invoice.created) |
X-Webhook-Id | Unique delivery ID |
User-Agent | Gaeld-Webhook/1.0 |
Signature Verification
Every webhook request is signed using HMAC-SHA256 with the webhook's secret. Always verify the signature before processing the payload.
PHP
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$computed = hash_hmac('sha256', $payload, $webhookSecret);
if (! hash_equals($computed, $signature)) {
http_response_code(400);
exit('Invalid signature');
}
$event = json_decode($payload, true);
// Process $event
Node.js
const crypto = require('crypto');
function verifySignature(payload, signature, secret) {
const computed = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(computed),
Buffer.from(signature)
);
}
// In your Express handler:
app.post('/webhooks/gaeld', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'];
if (!verifySignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(400).send('Invalid signature');
}
const event = JSON.parse(req.body);
// Process event
res.sendStatus(200);
});
Python
import hmac
import hashlib
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
computed = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(computed, signature)
Delivery & Retries
| Property | Value |
|---|---|
| HTTP timeout | 10 seconds |
| Max attempts | 3 |
| Retry backoff | 30 seconds, 5 minutes, 1 hour |
| Expected response | Any 2xx status code |
If your endpoint returns a non-2xx status or times out, Gäld retries with exponential backoff. After 3 failed attempts the delivery is marked as failed. You can check delivery status via the webhook API.
Managing Webhooks
| Action | Method | Endpoint |
|---|---|---|
| List all | GET | /api/v1/webhooks |
| View details | GET | /api/v1/webhooks/{id} |
| Create | POST | /api/v1/webhooks |
| Update events/URL | PUT | /api/v1/webhooks/{id} |
| Delete | DELETE | /api/v1/webhooks/{id} |
| Regenerate secret | POST | /api/v1/webhooks/{id}/regenerate-secret |
After calling regenerate-secret, update your application with the new secret immediately. The old secret is invalidated and all subsequent deliveries will use the new one.
Best Practices
- Always verify signatures — never trust payload content without checking the HMAC signature
- Respond quickly — return
200immediately and process the event asynchronously (e.g. via a job queue) - Handle duplicates — use the
X-Webhook-Idheader to deduplicate if your endpoint is called more than once - Use HTTPS — webhook URLs must use HTTPS in production
- Monitor failures — check webhook delivery status periodically and fix failing endpoints