Skip to content

Webhooks

Webhooks allow your application to receive real-time HTTP notifications when events occur in your Postproxy account, such as a post being processed, a platform post being published or failing, or a profile being disconnected.

When an event occurs, Postproxy sends a POST request to your configured URL with a JSON payload describing the event.

EventDescription
post.processedA post has been processed and is ready for publishing
platform_post.publishedA post was successfully published to a platform
platform_post.failedA post failed to publish to a platform
platform_post.failed_waiting_for_retryA post failed but will be retried
platform_post.insightsNew insights/analytics are available for a platform post
profile.disconnectedA connected social profile was disconnected
profile.connectedA new social profile was connected
media.failedA media attachment failed to process or download

You can subscribe to all events using * or select individual event types.


Every webhook delivery uses a consistent envelope structure:

{
"id": "evt_a1b2c3d4e5",
"type": "platform_post.published",
"created_at": "2025-01-15T10:30:00Z",
"data": {
// Event-specific data
}
}
HeaderDescription
Content-Typeapplication/json
User-AgentPostproxy-Webhooks/1.0
X-Postproxy-EventThe event type (e.g. platform_post.published)
X-Postproxy-DeliveryUnique delivery ID for this event
X-Postproxy-SignatureHMAC signature for verification

Uses the Post payload shape.

{
"id": "evt_abc123",
"type": "post.processed",
"created_at": "2025-01-15T10:30:00Z",
"data": {
"id": "abc123xyz",
"body": "Check out our latest update!",
"status": "processed",
"scheduled_at": null,
"created_at": "2025-01-15T10:30:00Z",
"platforms": [
{
"id": "prof_xyz",
"platform": "twitter",
"name": "My Twitter"
}
]
}
}
FieldTypeDescription
data.idstringPost hashid
data.bodystringPost content
data.statusstringPost status
data.scheduled_atstring | nullISO 8601 scheduled time
data.created_atstringISO 8601 creation time
data.platformsarrayProfiles attached to the post
data.platforms[].idstringProfile hashid
data.platforms[].platformstringNetwork name (e.g. twitter, linkedin)
data.platforms[].namestringProfile display name

platform_post.published, platform_post.failed, platform_post.failed_waiting_for_retry

Section titled “platform_post.published, platform_post.failed, platform_post.failed_waiting_for_retry”

These three events share the Platform Post payload shape. The status and error fields reflect the outcome.

{
"id": "evt_def456",
"type": "platform_post.published",
"created_at": "2025-01-15T10:30:01Z",
"data": {
"id": "pp_abc123",
"post_id": "abc123xyz",
"platform": "twitter",
"profile_id": "prof_xyz",
"profile_name": "My Twitter",
"status": "published",
"error": null,
"platform_id": "1234567890"
}
}
FieldTypeDescription
data.idstringPlatform post hashid
data.post_idstringParent post hashid
data.platformstringNetwork name
data.profile_idstringProfile hashid
data.profile_namestringProfile display name
data.statusstringpublished, failed, or failed_waiting_for_retry
data.errorstring | nullError message (null on success)
data.platform_idstring | nullExternal post ID on the platform (null until published)

Uses the Platform Post payload shape plus an insights field containing the latest analytics snapshot.

{
"id": "evt_ins789",
"type": "platform_post.insights",
"created_at": "2025-01-15T18:00:00Z",
"data": {
"id": "pp_abc123",
"post_id": "abc123xyz",
"platform": "twitter",
"profile_id": "prof_xyz",
"profile_name": "My Twitter",
"status": "published",
"error": null,
"platform_id": "1234567890",
"insights": {
"impressions": 1523,
"likes": 42,
"comments": 7,
"shares": 3
}
}
}
FieldTypeDescription
data.insightsobjectKey-value stats from the platform (varies by network)

All other fields are identical to the Platform Post shape above.


These two events share the Profile payload shape.

{
"id": "evt_prof01",
"type": "profile.disconnected",
"created_at": "2025-01-15T12:00:00Z",
"data": {
"id": "prof_xyz",
"name": "My Twitter",
"platform": "twitter",
"status": "disconnected",
"uid": "twitter_456",
"username": "myhandle"
}
}
FieldTypeDescription
data.idstringProfile hashid
data.namestringProfile display name
data.platformstringNetwork name
data.statusstringactive or disconnected
data.uidstringExternal ID on the platform
data.usernamestring | nullExternal username on the platform

Uses the Attachment payload shape.

{
"id": "evt_med01",
"type": "media.failed",
"created_at": "2025-01-15T10:30:02Z",
"data": {
"id": "att_xyz",
"post_id": "abc123xyz",
"content_type": "image/jpeg",
"status": "failed",
"error_message": "Media file not found"
}
}
FieldTypeDescription
data.idstringAttachment hashid
data.post_idstringParent post hashid
data.content_typestringMIME type of the attachment
data.statusstringAlways failed for this event
data.error_messagestringHuman-readable error description

Every webhook request includes an X-Postproxy-Signature header for verifying that the request came from Postproxy.

The signature format is:

t=1705312200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  • t — Unix timestamp when the signature was generated
  • v1 — HMAC-SHA256 hex digest

The signed payload is {timestamp}.{body} where body is the raw JSON request body.

Ruby

def verify_webhook(payload, signature_header, secret)
parts = signature_header.split(",").map { |p| p.split("=", 2) }.to_h
timestamp = parts["t"]
expected = parts["v1"]
signed_payload = "#{timestamp}.#{payload}"
computed = OpenSSL::HMAC.hexdigest("SHA256", secret, signed_payload)
ActiveSupport::SecurityUtils.secure_compare(computed, expected)
end

Node.js

const crypto = require("crypto");
function verifyWebhook(payload, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(",").map((p) => p.split("=", 2))
);
const timestamp = parts.t;
const expected = parts.v1;
const signedPayload = `${timestamp}.${payload}`;
const computed = crypto
.createHmac("sha256", secret)
.update(signedPayload)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(computed),
Buffer.from(expected)
);
}

Python

import hmac
import hashlib
def verify_webhook(payload: str, signature_header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in signature_header.split(","))
timestamp = parts["t"]
expected = parts["v1"]
signed_payload = f"{timestamp}.{payload}"
computed = hmac.new(
secret.encode(), signed_payload.encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(computed, expected)

If your endpoint returns a non-2xx status code or the request fails, Postproxy retries the delivery up to 5 times with exponential backoff:

AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 hours

After all attempts are exhausted, the delivery is marked as failed. You can view delivery history via the API.

Your endpoint should return a 2xx response within 30 seconds. Connections that take longer than 10 seconds to establish will time out.


MethodEndpointDescription
GET/api/webhooksList all webhooks
GET/api/webhooks/:idGet a single webhook
POST/api/webhooksCreate a webhook
PATCH/api/webhooks/:idUpdate a webhook
DELETE/api/webhooks/:idDelete a webhook
GET/api/webhooks/:id/deliveriesList delivery attempts

GET /api/webhooks

Terminal window
curl -X GET "https://api.postproxy.dev/api/webhooks" \
-H "Authorization: Bearer your_api_key"
{
"data": [
{
"id": "wh_abc123",
"url": "https://example.com/webhooks",
"events": ["post.processed", "platform_post.published"],
"enabled": true,
"description": "Production webhook",
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-15T10:00:00Z"
}
]
}

GET /api/webhooks/:id

Terminal window
curl -X GET "https://api.postproxy.dev/api/webhooks/wh_abc123" \
-H "Authorization: Bearer your_api_key"
{
"id": "wh_abc123",
"url": "https://example.com/webhooks",
"events": ["post.processed", "platform_post.published"],
"enabled": true,
"description": "Production webhook",
"secret": "whsec_a1b2c3d4e5f6...",
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-15T10:00:00Z"
}

The secret field is only included when fetching a single webhook or immediately after creation.


POST /api/webhooks

ParameterTypeRequiredDescription
urlstringYesHTTPS URL to receive webhook events
eventsarrayYesEvent types to subscribe to (use ["*"] for all)
descriptionstringNoDescription for the webhook
Terminal window
curl -X POST "https://api.postproxy.dev/api/webhooks" \
-H "Authorization: Bearer your_api_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks",
"events": ["post.processed", "platform_post.published"],
"description": "Production webhook"
}'
{
"id": "wh_abc123",
"url": "https://example.com/webhooks",
"events": ["post.processed", "platform_post.published"],
"enabled": true,
"description": "Production webhook",
"secret": "whsec_a1b2c3d4e5f6...",
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-15T10:00:00Z"
}

PATCH /api/webhooks/:id

ParameterTypeRequiredDescription
urlstringNoNew HTTPS URL
eventsarrayNoNew event types
enabledbooleanNoEnable or disable the webhook
descriptionstringNoUpdated description
Terminal window
curl -X PATCH "https://api.postproxy.dev/api/webhooks/wh_abc123" \
-H "Authorization: Bearer your_api_key" \
-H "Content-Type: application/json" \
-d '{
"enabled": false
}'
{
"id": "wh_abc123",
"url": "https://example.com/webhooks",
"events": ["post.processed", "platform_post.published"],
"enabled": false,
"description": "Production webhook",
"secret": "whsec_a1b2c3d4e5f6...",
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-15T12:00:00Z"
}

DELETE /api/webhooks/:id

Terminal window
curl -X DELETE "https://api.postproxy.dev/api/webhooks/wh_abc123" \
-H "Authorization: Bearer your_api_key"
{
"deleted": true
}

GET /api/webhooks/:id/deliveries

Retrieve delivery attempts for a specific webhook, useful for debugging.

ParameterTypeRequiredDefaultDescription
pageintegerNo0Page number (zero-indexed)
per_pageintegerNo20Number of deliveries per page
Terminal window
curl -X GET "https://api.postproxy.dev/api/webhooks/wh_abc123/deliveries" \
-H "Authorization: Bearer your_api_key"
{
"total": 15,
"page": 0,
"per_page": 20,
"data": [
{
"id": "abc123",
"event_id": "evt_abc123",
"event_type": "post.processed",
"response_status": 200,
"attempt_number": 1,
"success": true,
"attempted_at": "2025-01-15T10:30:01Z",
"created_at": "2025-01-15T10:30:01Z"
}
]
}