Webhook notifications
Receive project events, verify raw-body signatures and handle repeat deliveries.
Last updated:
On this page
Creating a subscriptionManagement endpointsEvent bodyEvent catalogueSignature verificationDelivery and retriesFixed verification vectorRetry intervalsCreating 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.
- Prepare an HTTPS receiver that accepts the request body without modification.
- Send
ad,url,olaylarand optionalproje_idstoPOST /api/webhooklar. - Securely store
sirfrom the creation response; subsequent lists do not expose the secret. - Create a test delivery with
POST /api/webhooklar/{wid}/dene. - Review the result in the delivery list; receiving
teslim_idalone 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 av1,prefix.x-gis-olayandx-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.