Zum Hauptinhalt springen

Webhooks

Webhooks benachrichtigen deinen HTTPS-Endpunkt, wenn in heycreo etwas fertig ist: ein Medium steht bereit, ein Export ist gerendert, oder eine Publication ist live.

Endpunkte legst du im Produkt unter Einstellungen → Webhooks an. Jeder Endpunkt hat ein eigenes Signing-Secret (whsec_…), das einmal angezeigt wird, wenn du den Endpunkt anlegst oder das Secret rotierst.

Schnellstart

  1. Endpunkt anlegen und die gewünschten Events abonnieren.
  2. Signing-Secret sofort kopieren — es wird nicht erneut angezeigt.
  3. Über Test einen Ping senden. heycreo schickt ein webhook.test-Event, damit du die Verbindung prüfst, bevor echte Events kommen.
  4. Signatur prüfen (unten) und mit 2xx antworten.

Delivery-Format

Jede Zustellung ist ein POST mit Content-Type: application/json und diesen Headern:

HeaderBedeutung
heycreo-signatureHMAC-Signatur (siehe Signatur prüfen)
heycreo-event-typeEvent-Typ, z. B. asset.created
heycreo-event-idStabile Event-Id (evt_…), gleich über Retries
heycreo-delivery-idId dieser Zustellung an diesen Endpunkt
heycreo-webhook-idId des empfangenden Endpunkts

Der Body hat immer dasselbe Envelope. Nur data ändert sich je Event-Typ.

{
"id": "evt_00000000-0000-4000-8000-000000000001",
"type": "asset.created",
"createdAt": "2026-01-15T10:04:12.000Z",
"organization": {
"id": "00000000-0000-4000-8000-0000000000aa",
"slug": "acme"
},
"data": {
"asset": {
"id": "00000000-0000-4000-8000-0000000000bb",
"name": "Sommerkampagne Hero",
"status": "completed",
"origin": "user_upload",
"mimeType": "image/png",
"fileSize": 1240000,
"width": 1920,
"height": 1080,
"durationMs": null,
"url": "https://files.example.com/asset.png",
"previewUrl": "https://files.example.com/asset-preview.webp",
"collectionId": null,
"tags": [{ "id": "00000000-0000-4000-8000-0000000000cc", "name": "Kampagne" }],
"customFields": {},
"aiContentSource": null,
"contentVersion": 1,
"createdAt": "2026-01-15T10:04:12.000Z"
}
}
}

Signatur prüfen

Der Header heycreo-signature hat diese Form:

heycreo-signature: t=1736937852,v1=8f9c…3ab1

v1 ist HMAC-SHA256 über "{t}.{rawBody}", mit deinem Endpoint-Secret als Schlüssel. rawBody muss der unparste Request-Body sein. JSON parsen und erneut serialisieren ändert Whitespace und lässt den Vergleich scheitern.

Lehne Requests ab, deren t mehr als fünf Minuten von deiner Uhr abweicht. Damit lassen sich mitgeschnittene Payloads nicht erneut einspielen.

import { createHmac, timingSafeEqual } from 'crypto'

function verify(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(',').map((p) => p.trim().split('=')),
)
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false

const expected = createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest('hex')

return timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected))
}
import hashlib
import hmac
import time

def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
parts = dict(
p.strip().split("=", 1) for p in signature_header.split(",")
)
if abs(time.time() - int(parts["t"])) > 300:
return False

expected = hmac.new(
secret.encode(),
f"{parts['t']}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()

return hmac.compare_digest(parts["v1"], expected)

Antworten und Retries

Antworte mit 2xx, sobald das Event gespeichert ist. Die eigentliche Arbeit asynchron erledigen — der Request läuft nach 10 Sekunden in ein Timeout.

Fehlgeschlagene Zustellungen werden mit exponentiellem Backoff bis zu 6 Mal wiederholt:

Deine Antwortheycreo
2xxZugestellt, kein Retry
408, 429, 5xxRetry
andere 4xxPermanent fehlgeschlagen, kein Retry
3xxPermanent fehlgeschlagen — Redirects werden nicht gefolgt

Scheitert ein Endpunkt wiederholt, deaktiviert heycreo ihn. Nach der Reparatur des Empfängers kannst du ihn unter Einstellungen → Webhooks wieder aktivieren.

Idempotenz

Dedupliziere über heycreo-event-id. Retries verwenden dieselbe Id, und derselbe Vorgang erzeugt nie zwei verschiedene Event-Ids.

Verwende nicht heycreo-delivery-id als Schlüssel. Ein Replay aus den Einstellungen erzeugt eine neue Zustellung für dasselbe Event.

Event-Typen

TypWann
asset.createdMedium ist verfügbar (Upload oder KI-Generierung). Herkunft über data.asset.origin.
asset.updatedDie Datei eines Mediums wurde ersetzt. data.asset.contentVersion steigt.
asset.deletedEin Medium wurde gelöscht.
export.completedEin Render war erfolgreich.
export.failedEin Render ist fehlgeschlagen. Meldung in data.export.error.
publication.publishedAlle Ziele einer Publication sind live.
publication.partially_publishedManche Ziele erfolgreich, andere fehlgeschlagen.
publication.failedEine Publication ohne erfolgreiches Ziel beendet.
approval.requestedEine Freigabe für ein Medium oder einen Post wurde geöffnet.
approval.decidedEin Reviewer hat zugestimmt oder abgelehnt. data.approval.decision ist die Stimme; status der Antrag insgesamt.

Ein Test-Ping sendet webhook.test. Darauf kannst du nicht abonnieren — es geht immer an den getesteten Endpunkt.

data.asset

FeldBeschreibung
idMedien-Id
nameAnzeigename
statusVerarbeitungsstatus (completed, wenn die Datei bereitsteht)
originHerkunft: user_upload, generate, csv_import, template_import oder api_import
mimeType, fileSize, width, height, durationMsDatei-Metadaten (durationMs bei Video/Audio)
url, previewUrlDatei- und Vorschau-URLs
collectionIdOrdner, falls vorhanden
tags{ id, name }
customFieldsWerte, keyed nach Custom-Field-Id
aiContentSourceKI-Inhaltskennzeichnung, oder null
contentVersionSteigt, wenn die Datei ersetzt wird
createdAtISO-Zeitstempel

data.export

FeldBeschreibung
id, postIdExport und zugehöriger Post
status, mediaTypeRender-Status und Ausgabeart
imageUrl, videoUrl, pdfUrlAusgabe-URLs (je nachdem, was zutrifft)
isCarousel, carouselImageUrlsKarussell-Ausgaben, falls vorhanden
width, height, aspectRatioAbmessungen
errorBei export.failed gesetzt

data.publication

FeldBeschreibung
id, postIdPublication und zugehöriger Post
statusGesamtstatus
scheduledAt, publishedAtISO-Zeitstempel
destinations[]Ergebnis je Plattform: platform, status, externalPostId, publishedUrl, error

data.approval

FeldBeschreibung
idId der Freigabeanfrage
entityTypeasset oder post
entityIdMedium oder Post in der Prüfung
statusAntrag insgesamt (pending, approved, rejected, cancelled)
decisionStimme des Reviewers bei approval.decided (approved oder rejected); null bei approval.requested
requestedByUserIdWer die Anfrage geöffnet hat
reviewerUserIdWer abgestimmt hat; null bei approval.requested
entityContentVersionContent-Version zum Zeitpunkt der Anfrage
closingDateFrist, oder null

Debugging

Unter Einstellungen → Webhooks gibt es ein Delivery-Log für 30 Tage. Jeder Eintrag enthält den gesendeten JSON-Body, die Antwort deines Servers und eventuelle Fehler. Fehlgeschlagene Zustellungen kannst du von dort erneut senden.

Siehe auch

  • AssetscustomFields zurückschreiben, nachdem du asset.created erhalten hast