Webhook notifications

Receive project events, verify raw-body signatures and handle repeat deliveries.

Last updated:

On this pageCreating a subscriptionManagement endpointsEvent bodyEvent catalogueSignature verificationDelivery and retriesFixed verification vectorRetry intervals

Creating a subscription

Webhook management uses an organisation administrator's session cookie. An access key does not replace the management session. GIS_WEBHOOK=kapali disables both management and delivery.

  1. Prepare an HTTPS receiver that accepts the request body without modification.
  2. Send ad, url, olaylar and optional proje_ids to POST /api/webhooklar.
  3. Securely store sir from the creation response; subsequent lists do not expose the secret.
  4. Create a test delivery with POST /api/webhooklar/{wid}/dene.
  5. Review the result in the delivery list; receiving teslim_id alone does not establish successful receipt.
{
  "ad": "Proje bildirimleri",
  "url": "https://entegrasyon.example/webhook",
  "olaylar": ["degisiklik", "tasarim", "aktarim"],
  "proje_ids": [123]
}

The URL and project identifier are examples. proje_ids: null covers the organisation's main projects. Valid events are degisiklik, tasarim and aktarim; test deliveries use dene. Raw edits in a design branch are not delivered as a separate project publication.

Management endpoints

Method and endpoint Function
GET /api/webhooklar Lists subscriptions
POST /api/webhooklar Creates a subscription; returns 201 and a one-time sir
PATCH /api/webhooklar/{wid} Changes name, URL, events, project scope or aktif state
DELETE /api/webhooklar/{wid} Revokes a subscription; 204
POST /api/webhooklar/{wid}/sir Rotates the secret and returns the new sir
POST /api/webhooklar/{wid}/dene Queues a test delivery
GET /api/webhooklar/{wid}/teslimler Pages through delivery history with items and next
POST /api/webhooklar/{wid}/teslimler/{tid}/yeniden Returns a delivery to pending state

The delivery list accepts after and limit. Use govde=1&limit=1 to read the body. Rotation replaces the previous secret; update receiver configuration during the same transition.

Event body

Notifications carry olay, olay_id, zaman and kurum_id; project events include proje_id. Change notifications provide the batch, change-identifier range, counters and an api.changes path for details. Design notifications carry the design identifier and state transition; import notifications carry the import identifier, source, target and counters.

The body is not a complete project or geometry copy. Apply v1 API permissions separately when retrieving details. The event body's zaman and delivery signature timestamp are different concepts.

Event catalogue

x-gis-olay Additional body fields Meaning
degisiklik batch, kind, degisiklik, sayilar, kullanici_id, anahtar_id, api.changes Change range and counters for a batch in the main project.
tasarim tasarim.id, tur, eylem, onceki, sonraki Recorded design state transition. Fields are within tasarim.
aktarim aktarim.import_id, kaynak_id, mod, hedef, sayilar Source import target and result. Fields are within aktarim.
dene Common event fields Test delivery requested from management; does not indicate a project-data change.

For project notifications, evaluate kurum_id, proje_id, olay, olay_id and zaman together. kesildi: true may appear when some counters are truncated. Notifications describe event scope rather than raw features or geometry.

Signature verification

Every delivery carries these headers:

  • webhook-id: event identifier retained across deliveries.
  • webhook-timestamp: Unix seconds, renewed on every delivery attempt.
  • webhook-signature: Base64 HMAC-SHA256 signature with a v1, prefix.
  • x-gis-olay and x-gis-surum: event type and application version.

Headers and signed-message format follow Standard Webhooks. The examples below perform the same HMAC-SHA256 verification without an additional verification package. Obtain the signing key by decoding the Base64 portion after whsec_. The signed bytes are webhook-id + "." + webhook-timestamp + "." + raw HTTP body. Parsing and serialising JSON again may invalidate the signature.

For a ready-made verifier, see the Standard Webhooks reference libraries for Python, JavaScript/TypeScript and Go. Pass the raw body, original headers and subscription secret to the library's verification function. Verify your chosen library version with the test vector below and your receiver's time policy.

The examples compare signatures in constant time and apply a five-minute acceptance window. That window is an example receiver policy; it does not mean the server requires the same window.

import base64
import binascii
import hashlib
import hmac
import math
import re
import time


def webhook_dogrula(sir, basliklar, ham_govde, simdi=None):
    h = {k.lower(): v for k, v in basliklar.items()}
    olay_id = h.get("webhook-id", "")
    damga = h.get("webhook-timestamp", "")
    imzalar = h.get("webhook-signature", "").split()
    if not isinstance(ham_govde, bytes) or not sir.startswith("whsec_"):
        raise ValueError("Invalid webhook input")
    if not re.fullmatch(r"[A-Za-z0-9_-]{1,100}", olay_id):
        raise ValueError("Invalid event identifier")
    if not re.fullmatch(r"[0-9]{1,12}", damga):
        raise ValueError("Invalid timestamp")
    simdi = time.time() if simdi is None else simdi
    if not isinstance(simdi, (int, float)) or not math.isfinite(simdi):
        raise ValueError("A valid current time is required")
    if abs(simdi - int(damga)) > 300:
        raise ValueError("Signature timestamp is outside the acceptance window")
    try:
        anahtar = base64.b64decode(sir[6:], validate=True)
    except (ValueError, binascii.Error) as hata:
        raise ValueError("Invalid webhook secret") from hata
    if len(anahtar) != 32:
        raise ValueError("Invalid webhook secret")
    ileti = f"{olay_id}.{damga}.".encode("ascii") + ham_govde
    beklenen = base64.b64encode(
        hmac.new(anahtar, ileti, hashlib.sha256).digest()
    ).decode("ascii")
    if not any(
        parca.startswith("v1,") and hmac.compare_digest(parca[3:], beklenen)
        for parca in imzalar
    ):
        raise ValueError("Invalid webhook signature")
    return olay_id
import { createHmac, timingSafeEqual } from 'node:crypto';

export function webhookDogrula(sir, basliklar, hamGovde, simdi = Date.now() / 1000) {
  const h = Object.fromEntries(Object.entries(basliklar).map(([k, v]) => [k.toLowerCase(), v]));
  const id = h['webhook-id'] || '';
  const damga = h['webhook-timestamp'] || '';
  if (!Buffer.isBuffer(hamGovde) || !/^whsec_[A-Za-z0-9+/]{43}=$/.test(sir)) {
    throw new Error('Invalid webhook input');
  }
  if (!/^[A-Za-z0-9_-]{1,100}$/.test(id) || !/^[0-9]{1,12}$/.test(damga)) {
    throw new Error('Invalid webhook header');
  }
  if (!Number.isFinite(simdi) || Math.abs(simdi - Number(damga)) > 300) {
    throw new Error('Signature timestamp is outside the acceptance window');
  }
  const anahtar = Buffer.from(sir.slice(6), 'base64');
  if (anahtar.length !== 32 || anahtar.toString('base64') !== sir.slice(6)) {
    throw new Error('Invalid webhook secret');
  }
  const beklenen = createHmac('sha256', anahtar)
    .update(`${id}.${damga}.`, 'ascii').update(hamGovde).digest();
  const dogru = (h['webhook-signature'] || '').split(/\s+/).some(parca => {
    if (!/^v1,[A-Za-z0-9+/]{43}=$/.test(parca)) return false;
    const aday = Buffer.from(parca.slice(3), 'base64');
    return aday.length === beklenen.length &&
      aday.toString('base64') === parca.slice(3) && timingSafeEqual(aday, beklenen);
  });
  if (!dogru) throw new Error('Invalid webhook signature');
  return id;
}
package webhook

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "errors"
    "net/http"
    "regexp"
    "strconv"
    "strings"
    "time"
)

func Dogrula(sir string, baslik http.Header, hamGovde []byte, simdi time.Time) (string, error) {
    id, damga := baslik.Get("webhook-id"), baslik.Get("webhook-timestamp")
    if !regexp.MustCompile(`^[A-Za-z0-9_-]{1,100}$`).MatchString(id) ||
        !regexp.MustCompile(`^[0-9]{1,12}$`).MatchString(damga) {
        return "", errors.New("Invalid webhook header")
    }
    if !strings.HasPrefix(sir, "whsec_") {
        return "", errors.New("Invalid webhook secret")
    }
    anahtar, err := base64.StdEncoding.Strict().DecodeString(sir[6:])
    if err != nil || len(anahtar) != 32 {
        return "", errors.New("Invalid webhook secret")
    }
    saniye, err := strconv.ParseInt(damga, 10, 64)
    if err != nil { return "", errors.New("Invalid timestamp") }
    fark := simdi.Unix() - saniye
    if fark < -300 || fark > 300 {
        return "", errors.New("Signature timestamp is outside the acceptance window")
    }
    mac := hmac.New(sha256.New, anahtar)
    mac.Write([]byte(id + "." + damga + "."))
    mac.Write(hamGovde)
    beklenen := mac.Sum(nil)
    for _, parca := range strings.Fields(baslik.Get("webhook-signature")) {
        if !strings.HasPrefix(parca, "v1,") { continue }
        aday, err := base64.StdEncoding.Strict().DecodeString(parca[3:])
        if err == nil && hmac.Equal(aday, beklenen) { return id, nil }
    }
    return "", errors.New("Invalid webhook signature")
}

Obtain the raw body as bytes from your HTTP server. After signature verification, parse JSON and enforce persistent uniqueness for olay_id. Store the processed identifier and business result in the same transaction; on a repeat identifier, return success without reapplying the work. A timestamp check alone does not prevent repeated processing.

Delivery and retries

Only HTTP 2xx is successful. Failed deliveries are retried at increasing intervals; the event identifier stays the same and the signature timestamp is renewed. Persistent delivery states are bekliyor, tamam and vazgecildi. A repeatedly failing subscription may be suspended; check delivery history together with subscription activity.

The same event may arrive again if the receiver completes the work but its response is lost. Do not assume exactly-once delivery. Redirects are not followed; the target URL must point directly to the receiver. Target addresses are validated against installation network rules. On-premises installations can use HTTP only when explicitly enabled; HTTPS is the default.

Fixed verification vector

The following secret is public test data and must not be used for a real subscription. When encoding ham_govde as UTF-8 bytes, do not append a newline. Pass simdi to the verifier to keep the test independent of wall-clock time; use current time in a real receiver.

{
  "sir": "whsec_AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
  "simdi": 1794301200,
  "basliklar": {
    "webhook-id": "msg_ornek_202611",
    "webhook-timestamp": "1794301200",
    "webhook-signature": "v1,y7FOL9NFVYbYF7S9s3xUMs1jV8lwLCPYltF293j/mrs="
  },
  "ham_govde": "{\"olay\":\"dene\",\"olay_id\":\"msg_ornek_202611\",\"kurum_id\":7,\"zaman\":\"2026-11-10T09:00:00+00:00\"}",
  "beklenen": "msg_ornek_202611"
}

Valid input should return msg_ornek_202611. Changing the body, event identifier or signature must fail verification. A timestamp outside the acceptance window must also fail. When multiple signatures exist, one supported valid v1, entry is sufficient. The acceptance window and uniqueness record are separate checks.

Retry intervals

After failed delivery Base wait before the next attempt
First failure 5 seconds
Second failure 5 minutes
Third failure 30 minutes
Fourth failure 2 hours
Fifth failure 5 hours
Sixth failure 10 hours
Seventh failure 24 hours
Eighth failure vazgecildi state

A small random variation is added to the base wait; the table is not a delivery-time or service-duration commitment. Worker and network conditions affect actual retry times. A receiver may return 2xx after persistently queuing a verified event; do not hold the HTTP connection open for long-running work. Do not return success if queue persistence failed.

Update receiver configuration when rotating the subscription secret. Review delivery history before a manual retry, and use v1 API for key-authenticated data reads.