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.
Event types
Section titled “Event types”| Event | Description |
|---|---|
post.processed | A post has been processed and is ready for publishing |
platform_post.published | A post was successfully published to a platform |
platform_post.failed | A post failed to publish to a platform |
platform_post.failed_waiting_for_retry | A post failed but will be retried |
platform_post.insights | New insights/analytics are available for a platform post |
profile.disconnected | A connected social profile was disconnected |
profile.connected | A new social profile was connected |
media.failed | A media attachment failed to process or download |
You can subscribe to all events using * or select individual event types.
Event payload
Section titled “Event payload”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 }}Headers
Section titled “Headers”| Header | Description |
|---|---|
Content-Type | application/json |
User-Agent | Postproxy-Webhooks/1.0 |
X-Postproxy-Event | The event type (e.g. platform_post.published) |
X-Postproxy-Delivery | Unique delivery ID for this event |
X-Postproxy-Signature | HMAC signature for verification |
Payloads by event
Section titled “Payloads by event”post.processed
Section titled “post.processed”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" } ] }}| Field | Type | Description |
|---|---|---|
data.id | string | Post hashid |
data.body | string | Post content |
data.status | string | Post status |
data.scheduled_at | string | null | ISO 8601 scheduled time |
data.created_at | string | ISO 8601 creation time |
data.platforms | array | Profiles attached to the post |
data.platforms[].id | string | Profile hashid |
data.platforms[].platform | string | Network name (e.g. twitter, linkedin) |
data.platforms[].name | string | Profile 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" }}| Field | Type | Description |
|---|---|---|
data.id | string | Platform post hashid |
data.post_id | string | Parent post hashid |
data.platform | string | Network name |
data.profile_id | string | Profile hashid |
data.profile_name | string | Profile display name |
data.status | string | published, failed, or failed_waiting_for_retry |
data.error | string | null | Error message (null on success) |
data.platform_id | string | null | External post ID on the platform (null until published) |
platform_post.insights
Section titled “platform_post.insights”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 } }}| Field | Type | Description |
|---|---|---|
data.insights | object | Key-value stats from the platform (varies by network) |
All other fields are identical to the Platform Post shape above.
profile.connected, profile.disconnected
Section titled “profile.connected, profile.disconnected”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" }}| Field | Type | Description |
|---|---|---|
data.id | string | Profile hashid |
data.name | string | Profile display name |
data.platform | string | Network name |
data.status | string | active or disconnected |
data.uid | string | External ID on the platform |
data.username | string | null | External username on the platform |
media.failed
Section titled “media.failed”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" }}| Field | Type | Description |
|---|---|---|
data.id | string | Attachment hashid |
data.post_id | string | Parent post hashid |
data.content_type | string | MIME type of the attachment |
data.status | string | Always failed for this event |
data.error_message | string | Human-readable error description |
Signature verification
Section titled “Signature verification”Every webhook request includes an X-Postproxy-Signature header for verifying that the request came from Postproxy.
The signature format is:
t=1705312200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdt— Unix timestamp when the signature was generatedv1— HMAC-SHA256 hex digest
The signed payload is {timestamp}.{body} where body is the raw JSON request body.
Verification examples
Section titled “Verification examples”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)endNode.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 hmacimport 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)Retry policy
Section titled “Retry policy”If your endpoint returns a non-2xx status code or the request fails, Postproxy retries the delivery up to 5 times with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 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.
API endpoints
Section titled “API endpoints”| Method | Endpoint | Description |
|---|---|---|
GET | /api/webhooks | List all webhooks |
GET | /api/webhooks/:id | Get a single webhook |
POST | /api/webhooks | Create a webhook |
PATCH | /api/webhooks/:id | Update a webhook |
DELETE | /api/webhooks/:id | Delete a webhook |
GET | /api/webhooks/:id/deliveries | List delivery attempts |
List webhooks
Section titled “List webhooks”GET /api/webhooks
Example
Section titled “Example”curl -X GET "https://api.postproxy.dev/api/webhooks" \ -H "Authorization: Bearer your_api_key"Response
Section titled “Response”{ "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 webhook
Section titled “Get webhook”GET /api/webhooks/:id
Example
Section titled “Example”curl -X GET "https://api.postproxy.dev/api/webhooks/wh_abc123" \ -H "Authorization: Bearer your_api_key"Response
Section titled “Response”{ "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.
Create webhook
Section titled “Create webhook”POST /api/webhooks
Parameters
Section titled “Parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | Yes | HTTPS URL to receive webhook events |
events | array | Yes | Event types to subscribe to (use ["*"] for all) |
description | string | No | Description for the webhook |
Example
Section titled “Example”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" }'Response (201 Created)
Section titled “Response (201 Created)”{ "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"}Update webhook
Section titled “Update webhook”PATCH /api/webhooks/:id
Parameters
Section titled “Parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | No | New HTTPS URL |
events | array | No | New event types |
enabled | boolean | No | Enable or disable the webhook |
description | string | No | Updated description |
Example
Section titled “Example”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 }'Response
Section titled “Response”{ "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 webhook
Section titled “Delete webhook”DELETE /api/webhooks/:id
Example
Section titled “Example”curl -X DELETE "https://api.postproxy.dev/api/webhooks/wh_abc123" \ -H "Authorization: Bearer your_api_key"Response
Section titled “Response”{ "deleted": true}List deliveries
Section titled “List deliveries”GET /api/webhooks/:id/deliveries
Retrieve delivery attempts for a specific webhook, useful for debugging.
Query parameters
Section titled “Query parameters”| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
page | integer | No | 0 | Page number (zero-indexed) |
per_page | integer | No | 20 | Number of deliveries per page |
Example
Section titled “Example”curl -X GET "https://api.postproxy.dev/api/webhooks/wh_abc123/deliveries" \ -H "Authorization: Bearer your_api_key"Response
Section titled “Response”{ "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" } ]}