GnistAI GnistAI
Log in

Webhook Consumer Guide

Receive real-time data change events and verify their authenticity.

Overview   |   MCP   |   REST API   |   CLI   |   Frameworks   |   Webhooks   |   Toolkits   |   OpenAPI   |   Home
1 Create a Subscription

Subscribe to an entity type with a public HTTPS callback URL. The response includes a secretsave it now, it is only shown once.

Shell
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"}'
Response (201)
{
  "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.

TierMax Subscriptions
Free5
Starter20
Pro100
EnterpriseUnlimited
2 Understand the Payload

When data changes, Gnist Context sends a POST request to your callback URL with this JSON payload:

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"
  }
}
FieldTypeDescription
entity_typestringThe data source (e.g. brreg_company, doffin_tender)
entity_idstringUnique identifier within the source
changed_atstringISO 8601 UTC timestamp of the change
dataobjectFull snapshot of the entity after the change

The request includes these headers:

HeaderValue
Content-Typeapplication/json
X-Gnist-Signaturesha256={hex_digest}
3 Verify the Signature

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.

Python
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)
Flask example
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
Node.js
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")
  );
}
Express example
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);
});
4 Handle Retries

Gnist Context expects your endpoint to respond with a 2xx status code within 10 seconds. On failure, deliveries are retried up to 3 times:

AttemptDelay
1Immediate
230 seconds
35 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:

Shell
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_id and changed_at fields to deduplicate
5 Manage Subscriptions

List your active subscriptions:

Shell
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):

Shell
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 TypeSourceDescription
brreg_companyBrregNorwegian company registry changes
doffin_tenderDoffinPublic procurement tender updates
einnsyn_recordeInnsynPublic document changes
stortinget_caseStortingetParliamentary case updates
nve_flood_areaNVEFlood hazard area alerts
6 Next Steps