Docs / Architecture / Webhooks

Webhooks

Ploton delivers task results and lifecycle events asynchronously via webhooks — here's how to receive, verify, and handle them.

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:

EventWhen it firesWhat your app should do
task.completeTask finished successfullyProcess the result data, resume your agent’s workflow
task.failedTask encountered an unrecoverable errorRead the error details, decide to retry or escalate
task.progressTask hit a meaningful intermediate milestoneOptional: update a progress indicator, log for observability
task.waitingTask is paused, waiting for external inputPresent 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": "jane@acme.com" },
			{ "name": "Bob Chen", "email": "bob@acme.com" }
		]
	}
}

Payload fields

FieldTypeDescription
eventstringThe event type (see table above)
task_idstringThe ID of the task that triggered this event
timestampstringISO 8601 timestamp of when the event occurred
dataobjectEvent-specific payload. Contents vary by event type and task.

Event-specific payloads

task.completedata 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.faileddata 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.waitingdata 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:

  1. Respond 200 within 30 seconds. Ploton treats anything else as a failure.
  2. Route on the event field — task.complete, task.failed, task.waiting, task.progress.
  3. 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