Webhooks
Trigger task execution from an external source.
A webhook is a publicly accessible URL that triggers a task execution. When a webhook is triggered,
it passes its payload to the task through a
webhook_payload
JSON parameter.Webhooks can be useful in a variety of scenarios, such as triggering task execution from external
sources or integrating with third-party tools. For example, you can use webhooks to:
- Create a new issue in your bug tracking software when a customer reports a bug
- Deploy a new version of your application when your CI pipeline finishes building code
Adding a webhook to a task will create a publicly accessible URL. Anyone with the webhook URL will
be able to trigger the task, so treat the URL as a secret.
Create a webhook
Create a webhook
When a webhook is created, a unique secret URL is generated that is associated with a specific task
and environment. A task can have multiple webhooks.
You can declare webhooks in your task definition file or add a webhook using the Studio. Webhooks
are specified as a list of slugs, or as an object whose keys are the slugs and values are the
webhook configuration.
typescriptCopied1// my_task.airplane.ts2export default airplane.task(3{4slug: "my_task",5webhooks: { my_webhook: { requireAirplaneToken: false } },6// Shorthand with no configuration:7// webhooks: ["my_webhook"]8},9async () => {...}10);
pythonCopied1# my_task_airplane.py2@airplane.task(3webhooks=[4airplane.Webhook(5slug="my_webhook",6require_airplane_token=False,7),8],9)10def my_task(webhook_payload: airplane.JSON):11pass
In your task definition file (the file with extension
.task.yaml
), declare a slug for each
webhook. For example, a SQL task with a webhook would look like this:yamlCopied1# update_customer.task.yaml2slug: update_customer3name: update customer4sql:5resource: demo_db6entrypoint: update_customer.sql7queryArgs:8customer_id: "{{params.webhook_payload.customer.id}}"9webhooks:10my_webhook:11requireAirplaneToken: false12# Shorthand with no configuration:13# - my_webhook
You can create a new webhook in the Advanced section of the task editor:

Get the webhook URL
Get the webhook URL
Once your task is deployed, the webhook URL will be available on the task page:

You can paste the URL into any third-party tool that supports webhooks.
Trigger a webhook
Trigger a webhook
You can trigger a webhook by making a POST request to the webhook URL. The request body is expected
to be a JSON payload.
The payload is passed to the task as a JSON parameter with slug
webhook_payload
, which can be accessed in the task's code.The
webhook_payload
parameter contains the following fields:Field | Description |
---|---|
slug | The webhook's slug, which is used to identify which webhook was triggered. This is useful if your task has multiple webhooks. |
headers | The HTTP headers sent with the request, as an object. |
body | The parsed request body, as an object. |
rawBody | The raw request body as a base64 encoded string. This is useful if you want to parse the body yourself or implement authentication that relies on the raw body. |
You can then access the parameter in your task code. The exact syntax depends on the language you're
using:
typescriptCopied1// my_task.airplane.ts2export default airplane.task(3{4slug: "my_task",5webhooks: { my_webhook: { requireAirplaneToken: false } },6},7async (params) => {8const { body } = params.webhook_payload;9// Do something with the body.10}11);
pythonCopied1# my_task_airplane.py2@airplane.task(3webhooks=[4airplane.Webhook(5slug="my_webhook",6require_airplane_token=False,7),8],9)10def my_task(webhook_payload: airplane.JSON):11body = webhook_payload["body"]12# Do something with the body.
YAML based tasks can access the webhook parameter using Javascript templates.
For example, a SQL task can access the webhook parameter using the following syntax:
yamlCopied1# update_customer.task.yaml2slug: update_customer3name: update customer4sql:5resource: demo_db6entrypoint: update_customer.sql7queryArgs:8customer_id: "{{params.webhook_payload.customer.id}}"9webhooks:10my_webhook:11requireAirplaneToken: false
Testing
Testing
You can test your webhook by sending an HTTP POST request to the webhook's URL.
For example, if this payload is sent to the URL for the webhook with slug
my_webhook
via CURL
command:bashCopied1curl https://api.airplane.dev/v0/webhooks/prod/tsk20230616zclw0nuh37q/my_webhook_secret \2-H "Content-Type: application/json" \3-d '{4"cats": { "name": "Mittens", "age": 5 }5}'
The task will receive this JSON value in the
webhook_payload
parameter:JSONCopied1{2"slug": "my_webhook",3"headers": {4"Content-Type": "application/json"5},6"body": {7"cats": { "name": "Mittens", "age": 5 }8},9"rawBody": "eyJjYXRzIjogeyJuYW1lIjogIk1pdHRlbnMiLCAiYWdlIjogNX19"10}
You can also test your webhook by pasting your test payload into the
webhook_payload
parameter on
the task page, both in studio and once the task is deployed:
Authentication
Authentication
By default, Airplane does not require authentication for webhooks. Anyone with the webhook URL can
execute the underlying task.
Require an Airplane API key
Require an Airplane API key
If your upstream tool allows custom headers, you can configure your webhook to require an
Airplane API key. If
requireAirplaneToken
is true, only requests
with a valid API key in the X-Airplane-API-Key
header will have permission to execute the task.Custom authentication examples
Custom authentication examples
You can implement custom authentication in your task logic to prevent unauthorized access. One
common approach is to verify that a signature in the headers matches the body of the request. If
you're comparing a signature against the
rawBody
, make sure to decode it from base64 before
verifying the signature.Stripe
Stripe
Stripe recommends authenticating webhooks using a secret key. You can implement this in your task
using the Stripe SDK and the
rawBody
from the
webhook payload.typescriptCopied1// stripe_webhook.airplane.ts2import airplane from "airplane";3import { Stripe } from "stripe";45export default airplane.task(6{7slug: "stripe_events",8name: "Stripe events handler",9envVars: {10STRIPE_API_KEY: {11config: "stripe_api_key",12},13STRIPE_WEBHOOK_SECRET: {14config: "stripe_webhook_secret",15},16},17webhooks: ["stripe_prod"],18// Webhooks adds a webhook_payload parameter for your task19},20async (params) => {21// Retrieve the Stripe API key and webhook secret from your environment variables22const apiKey = process.env.STRIPE_API_KEY;23const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;2425// Create a new instance of the Stripe SDK using your API key.26const stripe = new Stripe(apiKey, { apiVersion: "2022-11-15" });2728// Verify the webhook signature using the raw body.29// Retrieve the Stripe webhook signature from the headers30const { rawBody, headers } = params.webhook_payload;31const decodedBody = Buffer.from(rawBody, "base64").toString("utf-8");32const stripeSignature = headers["Stripe-Signature"];33try {34const event = stripe.webhooks.constructEvent(decodedBody, stripeSignature, webhookSecret);35// At this point, the webhook signature is valid36// You can now handle the event based on its type37switch (event.type) {38case "payment_intent.created":39// Handle payment intent created event40break;41// Add more cases for other event types as needed42default:43// Handle unrecognized event types44break;45}46return { body: "Webhook signature verified successfully" };47} catch (err) {48// An error occurred during signature verification49throw new Error("Webhook signature verification failed: " + err);50}51},52);
Linear
Linear
Linear hashes the webhook content using a SHA256 HMAC signature delivered in the
Linear-Signature
header. You can verify this signature in your task using the crypto
module and the rawBody
from
the webhook payload.For more information, see
Linear's documentation.
typescriptCopied1import airplane from "airplane";2import crypto from "node:crypto";34export default airplane.task(5{6slug: "linear_events",7name: "Linear events handler",8envVars: {9// Create a webhook in the Linear UI: https://linear.app/settings/api10LINEAR_WEBHOOK_SECRET: {11config: "linear_webhook_secret",12},13},14webhooks: ["linear_issues"],15},16async (params) => {17// Retrieve the webhook secret from an environment variable.18const webhookSecret = process.env.LINEAR_WEBHOOK_SECRET;19if (!webhookSecret) {20throw new Error("Missing LINEAR_WEBHOOK_SECRET");21}2223const {24webhook_payload,25}: {26webhook_payload: {27body: Record<string, any>;28headers: Record<string, any>;29rawBody: string;30};31} = params as any;3233// Verify the webhook signature using the raw body.34const decodedBody = Buffer.from(webhook_payload.rawBody, "base64").toString("utf-8");35const signature = crypto.createHmac("sha256", webhookSecret).update(decodedBody).digest("hex");36if (signature !== webhook_payload.headers["Linear-Signature"]) {37throw new Error("Invalid signature");38}3940// At this point, the webhook signature is valid.41// You can now handle the event based on its type.42switch (webhook_payload.body.type) {43case "Issue":44// Handle changes to Linear issues45break;46}4748return params;49},50);
Reference
Reference
webhooks : { [slug: string]: { requireAirplaneToken?: boolean } }
slug
REQUIRED
Slugs must:
- Be fewer than 50 characters long.
- Use only lowercase letters, numbers and underscores.
- Start with a lowercase later.
requireAirplaneToken
optional
Default
false
Whether an Airplane API Key is required to execute the webhook. If true
, only requests with the X-Airplane-API-Key
header will have permission to execute the task.
airplane.Webhook(slug="my_webhook", requireAirplaneToken=True)
slug
REQUIRED
Slugs must:
- Be fewer than 50 characters long.
- Use only lowercase letters, numbers and underscores.
- Start with a lowercase later.
require_airplane_token
optional
Default
False
Whether an Airplane API Key is required to execute the webhook. If True
, only requests with the X-Airplane-API-Key
header will have permission to execute the task.