Overview
Ploton sends webhook events as POST requests to the endpoint you configure in your dashboard, so you don’t have to poll.
All payloads are JSON. Every request includes an X-Ploton-Signature header for verification.
Event types
| Event | Trigger | Contains |
|---|---|---|
task.complete | Task finished successfully | The task result data |
task.failed | Task hit an unrecoverable error | Error code, message, and recovery suggestion |
task.progress | Task reached a meaningful milestone | Progress description and intermediate data |
task.waiting | Task is paused for external input | Reason, service name, and action URL (e.g., OAuth) |
Payload schema
Every webhook payload has this shape:
| Field | Type | Description |
|---|---|---|
event | string | Event type (task.complete, task.failed, task.progress, task.waiting) |
task_id | string | The task that triggered this event |
timestamp | string | ISO 8601 timestamp |
data | object | Event-specific payload (see below) |
Event payloads
task.complete
Sent when a task finishes successfully. The data field has the structured result.
{
"event": "task.complete",
"task_id": "task_8xK2mP",
"timestamp": "2025-06-15T14:22:07Z",
"data": {
"contacts": [
{
"name": "Jane Smith",
"email": "[email protected]",
"company": "Acme Corp"
},
{
"name": "Bob Chen",
"email": "[email protected]",
"company": "Globex Inc"
}
]
}
}The shape of data depends on the task prompt and the tool that ran it.
task.failed
Sent when a task fails after exhausting retries. The data.error object has the details.
{
"event": "task.failed",
"task_id": "task_8xK2mP",
"timestamp": "2025-06-15T14:22:04Z",
"data": {
"error": {
"code": "auth_token_expired",
"message": "The user's OAuth token has expired and automatic refresh failed.",
"tool": "crm",
"recoverable": true,
"suggestion": "Prompt the user to re-authorize access"
}
}
}Error fields
| Field | Type | Description |
|---|---|---|
code | string | Machine-readable error code |
message | string | Human-readable explanation |
tool | string | Which tool hit the error |
recoverable | boolean | Whether retrying (possibly after user action) could fix it |
suggestion | string | Recommended next step |
Common error codes
| Code | Meaning | Typical fix |
|---|---|---|
auth_token_expired | OAuth token expired and couldn’t be refreshed | Re-prompt user for authorization |
service_unavailable | Third-party service returned 5xx errors | Retry after a delay |
rate_limited | Third-party service rate limit exceeded | Retry after backoff period |
invalid_credentials | API key or credentials for the service are invalid | Update credentials in the dashboard |
permission_denied | Insufficient OAuth scopes for the requested operation | Re-authorize with broader scopes |
workflow_depth_exceeded | Task generated more than 50 workflow steps | Simplify the prompt or split into multiple tasks |
task.progress
Sent at intermediate milestones during long-running tasks.
{
"event": "task.progress",
"task_id": "task_8xK2mP",
"timestamp": "2025-06-15T14:22:03Z",
"data": {
"step": "fetch_contacts",
"tool": "crm",
"message": "Fetched 150 of ~300 contacts",
"progress_pct": 50
}
}These are informational only. Use them for progress indicators if you want, or ignore them.
task.waiting
Sent when a task pauses because it needs something external, usually user OAuth consent.
{
"event": "task.waiting",
"task_id": "task_8xK2mP",
"timestamp": "2025-06-15T14:22:02Z",
"data": {
"reason": "oauth_consent_required",
"service": "crm",
"auth_url": "https://ploton.ai/auth/crm?session=sess_abc123",
"message": "User needs to authorize CRM access"
}
}Show the auth_url to the user. The task resumes automatically once they complete the flow.
Delivery behavior
flowchart LR
A["Event triggered"] --> B["Send webhook"]
B --> C{"200 OK?"}
C -->|Yes| D["Delivered"]
C -->|No| E["Retry 1m"]
E --> F{"200 OK?"}
F -->|Yes| D
F -->|No| G["Retry 5m → 30m → 2h"]
G --> H{"Delivered?"}
H -->|Yes| D
H -->|No| I["Undeliverable"]
style A fill:#1a1630,stroke:#FACC15,color:#e8e0f0
style B fill:#1a1630,stroke:#FACC15,color:#e8e0f0
style D fill:#1a1630,stroke:#50FA7B,color:#e8e0f0
style I fill:#1a1630,stroke:#FF5F56,color:#e8e0f0
Timing
Expect sub-second latency under normal conditions.
Retry schedule
If your endpoint doesn’t respond with 200 within 30 seconds, Ploton retries:
| Attempt | Delay |
|---|---|
| 1 | Immediate (first delivery) |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
After 5 failed attempts, the event is marked undeliverable. You can always fall back to GET /v1/tasks/:id to get task results regardless of whether the webhook landed.
Ordering
Events for a single task arrive in order. Across different tasks, ordering is not guaranteed.
Idempotency
Ploton may send the same event more than once — for example, if your server returned 200 but the acknowledgment got lost. Build your handler to be idempotent. Use task_id + event as your deduplication key.
Signature verification
Every webhook includes an X-Ploton-Signature header — an HMAC-SHA256 of the raw request body, signed with your webhook secret.
How it works
Compute HMAC-SHA256(your_webhook_secret, raw_request_body), hex-encode it, and compare against the X-Ploton-Signature header using a constant-time function.
JavaScript / TypeScript (Node.js)
import crypto from "crypto";
function verifyPlotonWebhook(rawBody: string, signature: string, secret: string): boolean {
const expected = crypto.createHmac("sha256", secret).update(rawBody, "utf8").digest("hex");
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}Python
import hmac
import hashlib
def verify_ploton_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature, expected)PHP
function verifyPlotonWebhook(string $rawBody, string $signature, string $secret): bool {
$expected = hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $signature);
}Rust
use hmac::{Hmac, Mac};
use sha2::Sha256;
fn verify_ploton_webhook(raw_body: &[u8], signature: &str, secret: &str) -> bool {
let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap();
mac.update(raw_body);
let expected = hex::encode(mac.finalize().into_bytes());
constant_time_eq::constant_time_eq(expected.as_bytes(), signature.as_bytes())
}Always use a constant-time comparison to prevent timing attacks (
timingSafeEqualin Node.js,hmac.compare_digestin Python,hash_equalsin PHP).
Next steps
- REST API — Endpoint reference for task operations
- Webhooks (Concepts) — How webhooks fit into the Ploton architecture
- Deploying — Production webhook configuration and security