Webhooks
Webhooks let you receive real-time HTTP callbacks when events happen in Wortfreunde. Instead of polling the API for changes, your server gets notified automatically.
Setup
Webhooks are configured in Wortfreunde Studio under Settings / Webhooks. You provide a URL, select which events to subscribe to, and receive a signature key for verifying deliveries.
Events
| Event | Description |
|---|---|
post.updated | A published post's content was updated |
post.publishing_pending | A post is ready to publish and awaiting confirmation via the API |
post.published | A post was published |
post.unpublished | A published post was reverted to draft |
API Platform Events
The post.publishing_pending and post.unpublished events are primarily used with API platform channels. When a post on an API platform channel reaches its scheduled time, Wortfreunde sets it to publishing_pending and fires the post.publishing_pending webhook. Your external system should then:
- Receive the webhook and process the post content
- Call
PATCH /channels/{channel_id}/posts/{id}/publishto confirm publication - Optionally provide
external_idandexternal_urlfor tracking
See Posts API, API Platform Publishing Workflow for the full flow.
Payload
Every webhook delivery sends a POST request to your URL with a JSON body:
{
"event": "post.updated",
"timestamp": "2026-03-09T15:48:46Z",
"account": {
"id": 3,
"name": "Wertstifter GmbH"
},
"data": {
"post": {
"id": 244,
"title": "Strukturelle Fokus-Voraussetzungen im Unternehmen",
"body": "Fokus ist keine Frage der Disziplin. Es ist eine Frage der Struktur...",
"teaser": null,
"slug": null,
"publication_status": "draft",
"created_at": "2026-03-09T15:48:31.673Z",
"updated_at": "2026-03-09T15:48:46.870Z",
"published_at": null,
"meta_title": null,
"meta_description": null,
"tags": [
{ "id": 514, "name": "Positionierung" },
{ "id": 425, "name": "Leadership" },
{ "id": 660, "name": "DeepWork" }
],
"media": [],
"channel": {
"id": 17,
"title": "LinkedIn",
"platform": "linkedin",
"team": null,
"posts_count": 6
}
}
}
}
Top-Level Fields
| Field | Type | Description |
|---|---|---|
event | string | Event type that triggered this delivery |
timestamp | string | ISO 8601 timestamp of when the event occurred |
account | object | The account that owns the resource |
account.id | integer | Account identifier |
account.name | string | Account display name |
data | object | Event-specific payload |
Post Object
The data.post object contains the full post at the time of the event. See the Posts API for field descriptions.
Signature Verification
Every webhook delivery is signed with HMAC-SHA256 using your signature key. Verify the following headers to ensure authenticity.
Headers
| Header | Description |
|---|---|
X-Wortfreunde-Signature | HMAC-SHA256 signature of the request body |
X-Wortfreunde-Timestamp | Unix timestamp of signature creation |
X-Wortfreunde-Event | Event type that triggered this delivery |
X-Wortfreunde-Delivery | Unique delivery identifier |
Verification Examples
Python
import hmac
import hashlib
def verify_webhook(payload_body, signature, timestamp, secret):
"""Verify a Wortfreunde webhook signature."""
message = f"{timestamp}.{payload_body}"
expected = hmac.new(
secret.encode(),
message.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
PHP
function verifyWebhook(string $payloadBody, string $signature, string $timestamp, string $secret): bool
{
$message = $timestamp . '.' . $payloadBody;
$expected = hash_hmac('sha256', $message, $secret);
return hash_equals($expected, $signature);
}
Ruby
require 'openssl'
def verify_webhook(payload_body, signature, timestamp, secret)
message = "#{timestamp}.#{payload_body}"
expected = OpenSSL::HMAC.hexdigest('SHA256', secret, message)
Rack::Utils.secure_compare(expected, signature)
end
Always verify the signature before processing a webhook. Without verification, an attacker could send forged payloads to your endpoint.
Verification Steps
- Extract
X-Wortfreunde-SignatureandX-Wortfreunde-Timestampfrom the request headers - Concatenate the timestamp and raw request body with a
.separator:{timestamp}.{body} - Compute HMAC-SHA256 of the concatenated string using your signature key
- Compare the computed signature with the header value using a timing-safe comparison
Check that X-Wortfreunde-Timestamp is within a reasonable window (e.g. 5 minutes) to prevent replay attacks.
Responding to Webhooks
Your endpoint should return a 2xx status code within 10 seconds to acknowledge receipt. If the delivery fails, Wortfreunde will retry with exponential backoff.
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
After 5 failed attempts, the delivery is marked as failed. You can view delivery history and manually retry from Settings / Webhooks in Studio.
Best Practices
- Respond quickly. Process the payload asynchronously (e.g. queue it) and return
200immediately. Don't block on database writes or external API calls. - Verify signatures. Always validate
X-Wortfreunde-Signaturebefore trusting the payload. - Handle duplicates. Use
X-Wortfreunde-Deliveryto deduplicate, the same event may be delivered more than once. - Use HTTPS. Your webhook URL should use HTTPS to protect the payload in transit.