Subscribe to an entity type with a public HTTPS callback URL. The response includes a secret — save it now, it is only shown once.
curl -X POST "https://context.gnist.ai/api/webhooks/subscriptions" \
-H "Gnist-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"entity_type": "brreg_company", "callback_url": "https://example.com/webhook"}'
{
"id": "d4e5f6a7-...",
"entity_type": "brreg_company",
"callback_url": "https://example.com/webhook",
"secret": "whsec_abc123...",
"is_active": true,
"created_at": "2026-04-15T08:00:00Z"
}
Callback URLs must use HTTPS and resolve to a public IP address. Localhost, private networks, and cloud metadata endpoints are rejected.
| Tier | Max Subscriptions |
|---|---|
| Free | 5 |
| Starter | 20 |
| Pro | 100 |
| Enterprise | Unlimited |
When data changes, Gnist Context sends a POST request to your callback URL with this JSON payload:
{
"entity_type": "brreg_company",
"entity_id": "912345678",
"changed_at": "2026-04-15T14:30:00+00:00",
"data": {
"name": "Example AS",
"organizationNumber": "912345678",
"status": "NORMAL"
}
}
| Field | Type | Description |
|---|---|---|
entity_type | string | The data source (e.g. brreg_company, doffin_tender) |
entity_id | string | Unique identifier within the source |
changed_at | string | ISO 8601 UTC timestamp of the change |
data | object | Full snapshot of the entity after the change |
The request includes these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
X-Gnist-Signature | sha256={hex_digest} |
Every delivery is signed with HMAC-SHA256 using your subscription secret. The signature is in the X-Gnist-Signature header as sha256={hex_digest}.
Important: The signature is computed over the payload serialized with sorted keys and compact separators ("," and ":"). Re-serialize the parsed JSON before comparing — do not sign the raw request body directly, as whitespace or key order differences will cause a mismatch.
import hashlib
import hmac
import json
def verify_signature(body: bytes, header: str, secret: str) -> bool:
"""Verify the X-Gnist-Signature header."""
if not header.startswith("sha256="):
return False
received = header[7:] # strip "sha256=" prefix
# Re-serialize with canonical encoding (sorted keys, compact)
canonical = json.dumps(
json.loads(body),
sort_keys=True,
ensure_ascii=False,
separators=(",", ":"),
).encode()
computed = hmac.new(
secret.encode(), canonical, hashlib.sha256
).hexdigest()
return hmac.compare_digest(received, computed)
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = "whsec_abc123..."
@app.post("/webhook")
def handle_webhook():
sig = request.headers.get("X-Gnist-Signature", "")
if not verify_signature(request.data, sig, WEBHOOK_SECRET):
abort(401)
event = request.json
print(f"{event['entity_type']} {event['entity_id']} changed")
return "", 200
const crypto = require("crypto");
function sortKeys(obj) {
if (Array.isArray(obj)) return obj.map(sortKeys);
if (obj !== null && typeof obj === "object") {
return Object.keys(obj).sort().reduce((acc, k) => {
acc[k] = sortKeys(obj[k]);
return acc;
}, {});
}
return obj;
}
function verifySignature(body, header, secret) {
if (!header.startsWith("sha256=")) return false;
const received = header.slice(7);
// Re-serialize with canonical encoding (sorted keys, compact)
const canonical = JSON.stringify(sortKeys(JSON.parse(body)));
const computed = crypto
.createHmac("sha256", secret)
.update(canonical)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(received, "hex"),
Buffer.from(computed, "hex")
);
}
const express = require("express");
const app = express();
const WEBHOOK_SECRET = "whsec_abc123...";
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.headers["x-gnist-signature"] || "";
if (!verifySignature(req.body, sig, WEBHOOK_SECRET)) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.body);
console.log(`${event.entity_type} ${event.entity_id} changed`);
res.sendStatus(200);
});
Gnist Context expects your endpoint to respond with a 2xx status code within 10 seconds. On failure, deliveries are retried up to 3 times:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 30 seconds |
| 3 | 5 minutes |
After all retries are exhausted, the delivery is marked as failed. Failed deliveries are automatically retried by a background recovery process with exponential backoff (15 min, 1 hour, 6 hours, 24 hours) and are abandoned after 24 hours.
To check delivery status, use the deliveries endpoint:
curl "https://context.gnist.ai/api/webhooks/deliveries?limit=10" \ -H "Gnist-API-Key: YOUR_API_KEY"
Best practices:
- Respond quickly (under 10s) — do heavy processing asynchronously
- Make your handler idempotent — the same event may be delivered more than once
- Use the
entity_idandchanged_atfields to deduplicate
List your active subscriptions:
curl "https://context.gnist.ai/api/webhooks/subscriptions" \ -H "Gnist-API-Key: YOUR_API_KEY"
Delete a subscription (soft delete — in-flight deliveries are not affected):
curl -X DELETE "https://context.gnist.ai/api/webhooks/subscriptions/{subscription_id}" \
-H "Gnist-API-Key: YOUR_API_KEY"
Available entity types for subscription:
| Entity Type | Source | Description |
|---|---|---|
brreg_company | Brreg | Norwegian company registry changes |
doffin_tender | Doffin | Public procurement tender updates |
einnsyn_record | eInnsyn | Public document changes |
stortinget_case | Stortinget | Parliamentary case updates |
nve_flood_area | NVE | Flood hazard area alerts |