Webhook Events
Listen to events in your Autopilot account on your HTTPS webhook endpoint so your integration can automatically trigger reactions. Receiving webhook events helps you respond to asynchronous events such as when a payout transaction is settled, a customer’s bank confirms a payment, or a customer disputes a charge.
Webhook events are triggered by events happening in Autopilot. This means your app will need to set up a public URL where you can receive HTTP events, detailed in the preparing for events section.
Subscribing to Events
To configure webhook events, you’ll need to configure your URL and select the events you want your app to receive. By default, events sent over webhooks are not guaranteed to be real-time or in order.
In the dashboard, navigate to the webhook configuration, page. Verify you are in the correct mode (test or live) then complete the following:
-
Click
Create Webhook
to register a new webhook endpoint -
Enter your webhook endpoints’ accessible URL so Autopilot knows where to deliver events
Webhook URL Format
The URL format to register a webhook endpoint is:
https://<your-website>/<your-webhook-endpoint> -
Select the event types you want to send to your webhook endpoint.
-
Click Create Webhook to save your new endpoint details.
-
Save webhook endpoint public key
With your new webhook endpoint, all requests will be signed using the endpoint public key. For security, you must validate all requests. Save the webhook public key listed here. For more information, see validating security request headers.
Once you have successfully created a webhook endpoint, you should see it listed on your dashboard.
Preparing for Events
Before you can add webhook Event URLs to your app, your endpoint must be prepared for two things ahead of time:
-
Acknowledging
ping
events from Autopilot -
Validate security-related request headers (
X-Signature-Ed25519
andX-Signature-Timestamp
)
If either of these is not completed, your webhook endpoint will be invalidated and will be automatically disabled.
Acknowledging ping
requests
When enabling your webhook endpoint, Autopilot will send a POST
request with a event
value of ping
. Your endpoint is expected to acknowledge the request by returning a 204
response with an empty body.
Validating Security Request Headers
To receive events via HTTPS, there are some security steps you must take. These ensure that unauthorized 3rd-party sources are unable to send data to your webhook, and only requests from Autopilot are trusted.
Each webhook is sent with the following headers:
X-Signature-Ed25519
as a hex-encoded signatureX-Signature-Timestamp
as a UNIX timestamp (in seconds)
Using your favourite security library, you must validate the request each time you receive an event. If the signature fails validation, your app should respond with a 401
error code. Code examples of validating security headers are available below in multiple languages.
In addition to ensuring your app validates security-related request headers at the time of enabling your endpoint, Autopilot will also perform automated, routine security checks against your endpoint, including purposefully sending you invalid signatures. If you fail the validation, we will disable your endpoint and alert you via email.
Responding to Events
When your webhook endpoint receives a webhook event, your app should respond with a 204
status code with no body within 5 seconds to acknowledge that your app successfully received it. If your app doesn’t respond to the webhook event, Autopilot will retry sending it several times using exponential backoff for up to 10 days.
If your webhook endpoint fails to respond too often, Autopilot will stop sending your webhook endpoint events and notify you via email.
Webhook Event Payload
Webhook events are wrapped in an outer payload, with an inner data
object.
Field | Type | Description |
---|---|---|
id | string | The unique event ID |
entityID | string | The ID of the associated entity |
mode | Operation Mode | The Autopilot operation mode for the event. Either live or test . |
type | Webhook Type | The webhook type, Either ping for ping events or event for webhook events |
version | string | The version scheme for the webhook event. Currently always 2025-01-01 |
data? | event body | Event data payload |
Event Body Object
Field | Type | Description |
---|---|---|
type | string | Event type |
timestamp | string | Timestamp of when the event occured in RFC3339 format |
object | object | Data for the event. The shape depends on the event type |
previousAttributes? | object | Previous attributes of the object. Only present in updated events. |
Event Types
Name | Value | Description |
---|---|---|
Payout Created | payout.created | Sent when a new payout transaction is created. |
Payout Updated | payout.updated | Sent when a payout transaction is updated. |
Validation Examples
JavaScript
const express = require('express');const nacl = require('tweetnacl');
const PUBLIC_KEY = "ENDPOINT_PUBLIC_KEY";
// Webhook endpointapp.post('/api/webhook', async (req, res) => { const timestamp = req.headers['X-Signature-Timestamp']; const signature = req.headers['X-Signature-ED25519'];
// Verify the signature const isValid = await verifyWebhookSignature(timestamp, req.rawBody, signature);
if (!isValid) { console.error('Invalid webhook signature'); return res.status(401).json({ error: 'Invalid signature' }); }
// Check if type is "ping" if (req.body.type === "ping") { return res.status(204).send(); }
// Respond with 2XX code to acknowledge the webhook. res.status(202).send();});
// Function to verify webhook signatureasync function verifyWebhookSignature(timestamp, payload, signature) { const FIVE_MINUTES = 5 * 60 * 1000; const currentTime = Date.now(); const timestampMillis = parseInt(timestamp, 10) * 1000;
if (Math.abs(currentTime - timestampMillis) > FIVE_MINUTES) { return false }
return nacl.sign.detached.verify( Buffer.from(timestamp + payload), Buffer.from(signature, "hex"), Buffer.from(PUBLIC_KEY, "hex") );}
Python
from flask import Flask, request, abortfrom nacl.signing import VerifyKeyfrom nacl.exceptions import BadSignatureErrorimport time
PUBLIC_KEY = "ENDPOINT_PUBLIC_KEY"verify_key = VerifyKey(bytes.fromhex(PUBLIC_KEY))
@app.route("/api/webhook", methods=["POST"])def webhook(): timestamp = request.headers.get("X-Signature-Timestamp") signature = request.headers.get("X-Signature-Ed25519") body = request.data.decode("utf-8")
if not timestamp or not signature: abort(400, "Missing required headers")
if not verify_webhook_signature(timestamp, body, signature): abort(401, "Invalid signature")
if request.json.get("type") == "ping": return "", 204
return "", 202
def verify_webhook_signature(timestamp, payload, signature): FIVE_MINUTES = 5 * 60 current_time = int(time.time()) timestamp_int = int(timestamp)
if abs(current_time - timestamp_int) > FIVE_MINUTES: return False
try: verify_key.verify(f"{timestamp}{payload}".encode(), bytes.fromhex(signature)) return True except BadSignatureError: return False
Java
import io.javalin.Javalin;import io.javalin.http.Context;import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;import org.bouncycastle.crypto.signers.Ed25519Signer;import org.bouncycastle.util.encoders.Hex;
private static final String PUBLIC_KEY = "ENDPOINT_PUBLIC_KEY";private static final byte[] verifyKey = Hex.decode(PUBLIC_KEY);
public static void main(String[] args) { var app = Javalin.create(/*config*/) .post("/webhook_events", Main::webhookEvent) .start(3000);}
private static void webhookEvent(Context ctx) { String timestamp = ctx.header("X-Signature-Timestamp"); String signature = ctx.header("X-Signature-Ed25519"); String body = ctx.body();
if (timestamp == null || signature == null) { ctx.status(401); ctx.result("missing required headers"); return; }
if (!verify_webhook_signature(timestamp, body, signature)) { ctx.status(401); ctx.result("invalid signature"); return; }
// Acknowledge webhook ctx.status(204);}
private static boolean verify_webhook_signature(String timestamp, String payload, String signature) { final int FIVE_MINUTES = 5 * 60; long currentTime = System.currentTimeMillis() / 1000; // Current time in seconds long timestampMillis = Long.parseLong(timestamp);
if (Math.abs(currentTime - timestampMillis) > FIVE_MINUTES) { return false; }
try { // Prepare the message for verification (timestamp + payload) byte[] message = (timestamp + payload).getBytes();
// Initialize the signer with the public key Ed25519PublicKeyParameters pubKey = new Ed25519PublicKeyParameters(verifyKey); Ed25519Signer signer = new Ed25519Signer(); signer.init(false, pubKey);
// Update the signer with the message signer.update(message, 0, message.length);
// Verify the signature return signer.verifySignature(Hex.decode(signature)); } catch (Exception e) { return false; }}