Overview
Ploton sends task results to your application via webhooks. You configure a single webhook endpoint in the Ploton dashboard, and Ploton sends HTTP POST requests to it as tasks progress and complete.
Your agent creates a task, keeps working, and gets a callback when there’s something to report. No polling. No blocking.
How it works
Your Agent Ploton Your Webhook Endpoint
| | |
|-- POST /v1/tasks ------>| |
|<-- 200 (task_id) -------| |
| |-- executes task ------------>|
| (continues working) | |
| |-- POST task.complete ------->|
| |<-- 200 OK -------------------|
| | |Event types
Ploton sends these webhook events:
| Event | When it fires | What your app should do |
|---|---|---|
task.complete | Task finished successfully | Process the result data, resume your agent’s workflow |
task.failed | Task encountered an unrecoverable error | Read the error details, decide to retry or escalate |
task.progress | Task hit a meaningful intermediate milestone | Optional: update a progress indicator, log for observability |
task.waiting | Task is paused, waiting for external input | Present the auth_url to the user, or surface the input request |
Payload format
Every webhook payload has the same shape:
{
"event": "task.complete",
"task_id": "task_8xK2mP",
"timestamp": "2025-06-15T14:22:03Z",
"data": {
"contacts": [
{ "name": "Jane Smith", "email": "[email protected]" },
{ "name": "Bob Chen", "email": "[email protected]" }
]
}
}Payload fields
| Field | Type | Description |
|---|---|---|
event | string | The event type (see table above) |
task_id | string | The ID of the task that triggered this event |
timestamp | string | ISO 8601 timestamp of when the event occurred |
data | object | Event-specific payload. Contents vary by event type and task. |
Event-specific payloads
task.complete — data contains the task result:
{
"event": "task.complete",
"task_id": "task_8xK2mP",
"timestamp": "2025-06-15T14:22:03Z",
"data": {
"deals": [
{ "name": "Acme Enterprise", "amount": 48000, "stage": "Negotiation" }
]
}
}task.failed — data contains error details:
{
"event": "task.failed",
"task_id": "task_8xK2mP",
"timestamp": "2025-06-15T14:22:03Z",
"data": {
"error": {
"code": "service_unavailable",
"message": "The connected service returned 503 after 3 retry attempts",
"tool": "crm",
"recoverable": true
}
}
}task.waiting — data describes what’s needed:
{
"event": "task.waiting",
"task_id": "task_8xK2mP",
"timestamp": "2025-06-15T14:22:03Z",
"data": {
"reason": "oauth_consent_required",
"service": "crm",
"auth_url": "https://ploton.ai/auth/crm?session=sess_xyz",
"message": "User needs to authorize CRM access"
}
}Handling webhooks
Your endpoint gets a POST with a JSON body. Parse the event field to decide what to do:
POST /webhook HTTP/1.1
Content-Type: application/json
X-Ploton-Signature: <hmac-sha256-hex>
{
"event": "task.complete",
"task_id": "task_8xK2mP",
"timestamp": "2025-06-15T14:22:03Z",
"data": { ... }
}Three rules:
- Respond
200within 30 seconds. Ploton treats anything else as a failure. - Route on the
eventfield —task.complete,task.failed,task.waiting,task.progress. - Do the real work asynchronously. Don’t block the response. And make your handler idempotent — Ploton may deliver the same event more than once.
Retry behavior
If your endpoint doesn’t return 200 (or takes longer than 30 seconds), Ploton retries with exponential backoff:
flowchart LR
A["Attempt 1<br/>Immediate"] -->|Fail| B["Attempt 2<br/>+1 min"]
B -->|Fail| C["Attempt 3<br/>+5 min"]
C -->|Fail| D["Attempt 4<br/>+30 min"]
D -->|Fail| E["Attempt 5<br/>+2 hours"]
E -->|Fail| F[Undeliverable]
A -->|200 OK| G[Delivered]
B -->|200 OK| G
C -->|200 OK| G
D -->|200 OK| G
E -->|200 OK| G
style G fill:#1a1630,stroke:#50FA7B,color:#e8e0f0
style F fill:#1a1630,stroke:#FF5F56,color:#e8e0f0
After 5 failed attempts, the event is marked undeliverable. You can still get the task result by calling GET /v1/tasks/:id directly.
Webhook security
Check the X-Ploton-Signature header to verify that a webhook actually came from Ploton. It contains an HMAC-SHA256 signature of the raw request body, signed with your webhook secret.
The signature is HMAC-SHA256(webhook_secret, raw_request_body), hex-encoded. Always compare using a constant-time function to avoid timing attacks.
JavaScript / TypeScript (Node.js)
import crypto from "crypto";
function verifyWebhook(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_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 verifyWebhook(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_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 verify signatures in production. Without verification, anyone who discovers your webhook URL can send fake events.
Next steps
- Authentication — API keys, webhook secrets, and OAuth
- API Reference: Webhooks — Full webhook payload reference
- Deploying — Production webhook configuration