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
- Endpunkt anlegen und die gewünschten Events abonnieren.
- Signing-Secret sofort kopieren — es wird nicht erneut angezeigt.
- Über Test einen Ping senden. heycreo schickt ein
webhook.test-Event, damit du die Verbindung prüfst, bevor echte Events kommen. - Signatur prüfen (unten) und mit
2xxantworten.
Delivery-Format
Jede Zustellung ist ein POST mit Content-Type: application/json und diesen
Headern:
| Header | Bedeutung |
|---|---|
heycreo-signature | HMAC-Signatur (siehe Signatur prüfen) |
heycreo-event-type | Event-Typ, z. B. asset.created |
heycreo-event-id | Stabile Event-Id (evt_…), gleich über Retries |
heycreo-delivery-id | Id dieser Zustellung an diesen Endpunkt |
heycreo-webhook-id | Id 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 Antwort | heycreo |
|---|---|
2xx | Zugestellt, kein Retry |
408, 429, 5xx | Retry |
andere 4xx | Permanent fehlgeschlagen, kein Retry |
3xx | Permanent 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
| Typ | Wann |
|---|---|
asset.created | Medium ist verfügbar (Upload oder KI-Generierung). Herkunft über data.asset.origin. |
asset.updated | Die Datei eines Mediums wurde ersetzt. data.asset.contentVersion steigt. |
asset.deleted | Ein Medium wurde gelöscht. |
export.completed | Ein Render war erfolgreich. |
export.failed | Ein Render ist fehlgeschlagen. Meldung in data.export.error. |
publication.published | Alle Ziele einer Publication sind live. |
publication.partially_published | Manche Ziele erfolgreich, andere fehlgeschlagen. |
publication.failed | Eine Publication ohne erfolgreiches Ziel beendet. |
approval.requested | Eine Freigabe für ein Medium oder einen Post wurde geöffnet. |
approval.decided | Ein 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
| Feld | Beschreibung |
|---|---|
id | Medien-Id |
name | Anzeigename |
status | Verarbeitungsstatus (completed, wenn die Datei bereitsteht) |
origin | Herkunft: user_upload, generate, csv_import, template_import oder api_import |
mimeType, fileSize, width, height, durationMs | Datei-Metadaten (durationMs bei Video/Audio) |
url, previewUrl | Datei- und Vorschau-URLs |
collectionId | Ordner, falls vorhanden |
tags | { id, name } |
customFields | Werte, keyed nach Custom-Field-Id |
aiContentSource | KI-Inhaltskennzeichnung, oder null |
contentVersion | Steigt, wenn die Datei ersetzt wird |
createdAt | ISO-Zeitstempel |
data.export
| Feld | Beschreibung |
|---|---|
id, postId | Export und zugehöriger Post |
status, mediaType | Render-Status und Ausgabeart |
imageUrl, videoUrl, pdfUrl | Ausgabe-URLs (je nachdem, was zutrifft) |
isCarousel, carouselImageUrls | Karussell-Ausgaben, falls vorhanden |
width, height, aspectRatio | Abmessungen |
error | Bei export.failed gesetzt |
data.publication
| Feld | Beschreibung |
|---|---|
id, postId | Publication und zugehöriger Post |
status | Gesamtstatus |
scheduledAt, publishedAt | ISO-Zeitstempel |
destinations[] | Ergebnis je Plattform: platform, status, externalPostId, publishedUrl, error |
data.approval
| Feld | Beschreibung |
|---|---|
id | Id der Freigabeanfrage |
entityType | asset oder post |
entityId | Medium oder Post in der Prüfung |
status | Antrag insgesamt (pending, approved, rejected, cancelled) |
decision | Stimme des Reviewers bei approval.decided (approved oder rejected); null bei approval.requested |
requestedByUserId | Wer die Anfrage geöffnet hat |
reviewerUserId | Wer abgestimmt hat; null bei approval.requested |
entityContentVersion | Content-Version zum Zeitpunkt der Anfrage |
closingDate | Frist, 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
- Assets —
customFieldszurückschreiben, nachdem duasset.createderhalten hast