GunSpec
Webhooks

Webhooks

Der umgekehrte Weg: Statt uns zu fragen, ob sich ein Datensatz geändert hat, teilen wir es Ihnen mit. Eine so gepflegte Kopie braucht überhaupt keine Abfrageschleife, denn die Zustellung enthält den Datensatz selbst.

Jedes Ereignis unten wird von der API heute akzeptiert. Diese Liste ist dieselbe, die Ihre Abonnement-Anfrage validiert, sie kann also nichts aufführen, was abgelehnt würde. Eine Änderung bedeutet, dass sich die von uns ausgelieferten Felder tatsächlich bewegt haben, entschieden durch denselben Vergleich, der updatedAt setzt; Ihr Webhook und Ihre bedingte Anfrage können sich also nie widersprechen.

EreignisWann es ausgelöst wird
Feuerwaffenfirearm.variant.updated wiederholt die Nutzlast für Datensätze mit einem übergeordneten Eintrag, sodass Sie eine Familie verfolgen können, ohne den gesamten Katalogverkehr aufzunehmen. Die Quellen- und Konfidenz-Ereignisse melden, dass sich die Herkunft einer Angabe geändert hat, nicht die Angabe selbst, und treffen zusammen mit firearm.updated ein.
firearm.createdEine Feuerwaffe wurde dem Katalog hinzugefügt.
firearm.updatedEine Feuerwaffe hat sich geändert: eines der ausgelieferten Felder hat sich bewegt.
firearm.deletedEine Feuerwaffe wurde entfernt. Die Nutzlast ist nur die ID.
firearm.variant.updatedEine Feuerwaffe mit übergeordnetem Eintrag wurde angelegt oder geändert. Die Nutzlast ist identisch mit firearm.updated und wird erneut gesendet, damit eine Familie für sich verfolgt werden kann.
firearm.source.changedDie Herkunft einer Angabe hat sich geändert, entweder die Quellenliste oder eine ballistische Referenz. Trifft zusammen mit firearm.updated ein.
firearm.confidence.changedDie Datenkonfidenz des Datensatzes hat sich geändert. Trifft zusammen mit firearm.updated ein.
ReferenzdatenHersteller, Patronen und Kategorien. Kleiner und träger als der Waffenkatalog. Ein eigenes Abonnement lohnt sich, wenn Sie sie als eigene Tabellen spiegeln.
manufacturer.createdEin Hersteller wurde dem Katalog hinzugefügt.
manufacturer.updatedEin Hersteller hat sich geändert: eines der ausgelieferten Felder hat sich bewegt.
manufacturer.deletedEin Hersteller wurde entfernt. Die Nutzlast ist nur die ID.
caliber.createdEine Patrone wurde dem Katalog hinzugefügt.
caliber.updatedEine Patrone hat sich geändert: eines der ausgelieferten Felder hat sich bewegt.
caliber.deletedEine Patrone wurde entfernt. Die Nutzlast ist nur die ID.
category.createdEine Kategorie wurde dem Katalog hinzugefügt.
category.updatedEine Kategorie hat sich geändert: eines der ausgelieferten Felder hat sich bewegt.
category.deletedEine Kategorie wurde entfernt. Die Nutzlast ist nur die ID.
BilderWird ausgelöst, wenn Bildmaterial einer Waffe hinzugefügt oder entfernt wird. Eine Waffe, die eine Darstellung erhält, ist auch eine Änderung des Datensatzes, daher wird zusätzlich firearm.updated ausgelöst.
image.createdEin Bild wurde dem Katalog hinzugefügt.
image.deletedEin Bild wurde entfernt. Die Nutzlast ist nur die ID.
Gesamter KatalogEin Ereignis, das für eine Änderung steht, die zu groß ist, um sie Datensatz für Datensatz zu beschreiben.
catalog.resyncedZu viele Datensätze haben sich gleichzeitig geändert, um sie einzeln zu beschreiben. Führen Sie eine erneute Synchronisierung über die API durch, statt es anzuwenden.

catalog.resynced trifft ein, wenn sich mehr als 1,000 Datensätze gleichzeitig ändern: der Katalog wird neu abgeleitet, es wird nicht etwas korrigiert. Führen Sie dann eine erneute Synchronisierung über die API durch; versuchen Sie nicht, es als Änderung einzelner Datensätze anzuwenden. Es erreicht jeden Endpunkt, der irgendein Ereignis abonniert hat, ob Sie dieses angefordert haben oder nicht, denn die Alternative wäre Stille an genau dem Tag, an dem Ihre ganze Kopie veraltet ist.

Ihre URL muss aus dem öffentlichen Internet erreichbar sein und schnell antworten. Wie viele Endpunkte Sie halten dürfen, hängt von Ihrem Tarif ab:

  • studio5 Endpunkt
  • enterprise20 Endpunkt
bash
# The signing secret is in this response and nowhere else.# Store it before you close the terminal.curl -sS -X POST \  -H "X-API-Key: $GUNSPEC_API_KEY" \  -H 'Content-Type: application/json' \  -d '{        "url": "https://your-app.example.com/hooks/gunspec",        "description": "catalog mirror",        "events": ["firearm.created", "firearm.updated", "firearm.deleted"]      }' \  https://api.gunspec.io/v1/me/webhooks # Send yourself a test delivery to prove the receiver before relying on it.curl -sS -X POST \  -H "X-API-Key: $GUNSPEC_API_KEY" \  https://api.gunspec.io/v1/me/webhooks/wh_.../test

Das Signaturgeheimnis wird nur beim Anlegen zurückgegeben, danach nie wieder. Es gibt keinen Endpunkt, der es später anzeigt. Wenn Sie es verlieren, löschen Sie den Endpunkt und registrieren einen neuen.

Ein POST mit einem Ereignis. Die Daten sind der Datensatz in derselben Form, in der der Detail-Endpunkt ihn für Ihren Tarif zurückgibt, version inklusive, und er kann ohne zweiten Aufruf direkt in Ihren Speicher übernommen werden. Eine Löschung enthält nur die ID. Es ist die einzige Änderung, die eine Abfrage nicht sehen kann, denn ein gehaltener Datensatz erscheint einfach nicht mehr, und das ist von einem falsch gesetzten Filter nicht zu unterscheiden.

http
POST /hooks/gunspec HTTP/1.1Content-Type: application/jsonX-Webhook-Id: evt_2f418305-4d11-4d2f-8075-994326c31276X-Webhook-Event: firearm.updatedX-Webhook-Delivery: b4c2bd96-f3a3-48e9-b6a4-b4c05489b632X-Webhook-Signature: t=1789012345,v1=9f86d081884c7d65... {  "id": "evt_2f418305-4d11-4d2f-8075-994326c31276",  "type": "firearm.updated",  "created_at": "2026-09-10T08:34:48.000Z",  "data": {    "id": "glock-g48-mos",    "name": "Glock G48 MOS",    "parentFirearmId": "glock-g48",    "barrelLengthMm": 106,    "version": "8c1f3a90d24b",    "updatedAt": "2026-09-10 08:34:48"  }}
X-Webhook-Id
Bezeichnet das Ereignis. Bleibt über Wiederholungen hinweg gleich und ist für jeden Endpunkt identisch, der dieselbe Änderung erhält. Dies ist der Wert zur Duplikaterkennung.
X-Webhook-Event
Der Ereignistyp, damit ein Empfänger weiterleiten kann, ohne zuerst den Rumpf zu parsen.
X-Webhook-Delivery
Dieser einzelne Versuch. Bei jeder Wiederholung anders. Nützlich als Referenz bei Rückfragen, aber der falsche Wert zur Duplikaterkennung.
X-Webhook-Signature
t=<Unix-Sekunden>,v1=<Hex>. Das Hex ist HMAC-SHA256 über den Zeitstempel, einen Punkt und den rohen Anfragerumpf, mit Ihrem Signaturgeheimnis als Schlüssel.

Prüfen Sie die Signatur, bevor Sie dem Rumpf vertrauen. Zwei Dinge gehen leicht schief: Signieren Sie die empfangenen Rohbytes und nicht ein neu serialisiertes Objekt, denn JSON.stringify darf Schlüssel umordnen und Abstände ändern; und vergleichen Sie in konstanter Zeit, damit der Vergleich selbst den erwarteten Wert nicht verrät.

typescript
import { createHmac, timingSafeEqual } from 'node:crypto' // Verify against the RAW body. A re-serialised object will not match:// JSON.stringify is free to reorder keys and change spacing.export function verify(rawBody: string, header: string, secret: string): boolean {  const parts = new Map(header.split(',').map((p) => p.split('=') as [string, string]))  const timestamp = parts.get('t')  const signature = parts.get('v1')  if (!timestamp || !signature) return false   // Reject anything older than five minutes so a captured delivery  // cannot be replayed at you later.  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false   const expected = createHmac('sha256', secret)    .update(`${timestamp}.${rawBody}`)    .digest('hex')   const a = Buffer.from(expected, 'hex')  const b = Buffer.from(signature, 'hex')  return a.length === b.length && timingSafeEqual(a, b)}

Antworten Sie mit 2xx, sobald die Signatur stimmt, und arbeiten Sie danach. Ein langsamer Empfänger ist von einem defekten nicht zu unterscheiden, und beide werden wiederholt, sodass ein Handler, der den Datensatz vor der Antwort importiert, bekommt ihn also erneut, während er noch arbeitet.

javascript
// Acknowledge first, process afterwards. A slow receiver is// indistinguishable from a broken one, and both get retried.app.post('/hooks/gunspec', async (req, res) => {  const raw = await readRawBody(req)  if (!verify(raw, req.headers['x-webhook-signature'], SECRET)) {    return res.status(401).end()  }   const event = JSON.parse(raw)   // Dedupe on the event id: a retry re-sends the same one, so this is  // what makes handling idempotent. The delivery id differs per attempt.  if (await seen(event.id)) return res.status(200).end()  await remember(event.id)   res.status(200).end()  await enqueue(event)})

Ein Nicht-2xx oder ein Timeout wird 3 Mal wiederholt, 1, 5 und 15 Minuten nach dem Fehlschlag, danach aufgegeben. Da eine Wiederholung dieselbe X-Webhook-Id trägt, ist doppelte Verarbeitung unproblematisch, wenn Sie darauf Duplikate erkennen.

Ein Webhook ist ein Änderungssignal, keine Zustellgarantie: Ein Endpunkt kann länger ausfallen als das Wiederholungsfenster, und eine Zustellung, die ihre Versuche aufgebraucht hat, wird nicht erneut gesendet. Behalten Sie die If-None-Match-Aktualisierungsschleife von der Caching-Seite als Absicherung und lassen Sie Webhooks entscheiden, wann sie läuft. So prüfen Sie nach einem Zeitplan, den Sie kontrollieren, und zahlen nichts für unveränderte Datensätze.