Docs / API Reference / Webhooks Reference

Webhooks Reference

Complete reference for Ploton webhook events — event types, payload schemas, delivery behavior, and signature verification.

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

EventTriggerContains
task.completeTask finished successfullyThe task result data
task.failedTask hit an unrecoverable errorError code, message, and recovery suggestion
task.progressTask reached a meaningful milestoneProgress description and intermediate data
task.waitingTask is paused for external inputReason, service name, and action URL (e.g., OAuth)

Payload schema

Every webhook payload has this shape:

FieldTypeDescription
eventstringEvent type (task.complete, task.failed, task.progress, task.waiting)
task_idstringThe task that triggered this event
timestampstringISO 8601 timestamp
dataobjectEvent-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": "jane@acme.com",
				"company": "Acme Corp"
			},
			{
				"name": "Bob Chen",
				"email": "bob@globex.com",
				"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

FieldTypeDescription
codestringMachine-readable error code
messagestringHuman-readable explanation
toolstringWhich tool hit the error
recoverablebooleanWhether retrying (possibly after user action) could fix it
suggestionstringRecommended next step

Common error codes

CodeMeaningTypical fix
auth_token_expiredOAuth token expired and couldn’t be refreshedRe-prompt user for authorization
service_unavailableThird-party service returned 5xx errorsRetry after a delay
rate_limitedThird-party service rate limit exceededRetry after backoff period
invalid_credentialsAPI key or credentials for the service are invalidUpdate credentials in the dashboard
permission_deniedInsufficient OAuth scopes for the requested operationRe-authorize with broader scopes
workflow_depth_exceededTask generated more than 50 workflow stepsSimplify 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:

AttemptDelay
1Immediate (first delivery)
21 minute
35 minutes
430 minutes
52 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 (timingSafeEqual in Node.js, hmac.compare_digest in Python, hash_equals in PHP).

Next steps