Webhooks
Expose HTTP endpoints that external services can call.
Why Use Webhooks
Your application needs to receive HTTP requests from external services: a payment provider notifying you of a completed charge, a source control platform reporting a new commit, or a monitoring tool sending an alert.
With webhooks, you decorate a function and deploy:
// A decorated method that handles incoming HTTP requests
@webhook('handleStripePayment')
async handleStripePayment(request: WebhookRequest): Promise<any> {
const invoiceId = request.body.data.object.id;
const customerId = request.body.data.object.customer;
await this.recordPayment(customerId, invoiceId);
return this.createWebhookResponse({ received: true }, 200);
}
No routes. No Express server. Just a function behind a URL.
Overview
Webhooks are backend functions exposed as HTTP endpoints. External services send requests to these endpoints, and your function processes the incoming data. Squid handles URL routing, request parsing, and response serialization.
When to use webhooks
| Use Case | Recommendation |
|---|---|
| Receive HTTP requests from external services | ✅ Webhook |
| Call a function from the Squid client | Use Executables |
| React to database changes | Use Triggers |
| Run code on a schedule | Use Schedulers |
How it works
- You decorate a method with
@webhook('webhookId')in a class that extendsSquidService - Squid discovers and registers the webhook at deploy time
- Squid exposes the webhook as an HTTP endpoint
- External services send requests to the endpoint URL
- Your function receives the request and returns a response
Quick Start
Prerequisites
- A Squid backend project initialized with
squid init-backend - The
@squidcloud/backendpackage installed
Step 1: Create a webhook
Create a service class that extends SquidService and add your webhook function:
import { SquidService, webhook, WebhookRequest } from '@squidcloud/backend';
export class ExampleService extends SquidService {
@webhook('hello')
async hello(request: WebhookRequest): Promise<any> {
const name = request.queryParams['name'] || 'World';
return this.createWebhookResponse({ message: `Hello, ${name}!` }, 200);
}
}
Step 2: Export the service
Ensure your service is exported from the service index file:
export * from './example-service';
Step 3: Start or deploy the backend
For local development, run the backend locally using the Squid CLI:
squid start
To deploy to the cloud, see deploying your backend.
Step 4: Test the webhook
After deployment, your webhook is available at:
https://[YOUR_APP_ID].[APP_REGION].squid.cloud/webhooks/hello?name=Squid
curl "https://[YOUR_APP_ID].[APP_REGION].squid.cloud/webhooks/hello?name=Squid"
The response:
{ "message": "Hello, Squid!" }
Core Concepts
Webhook URL format
After deployment, each webhook is accessible at a URL based on its ID:
Production:
https://[YOUR_APP_ID].[APP_REGION].squid.cloud/webhooks/[WEBHOOK_ID]
Dev environment:
https://[YOUR_APP_ID]-dev.[APP_REGION].squid.cloud/webhooks/[WEBHOOK_ID]
Local development:
https://[YOUR_APP_ID]-dev-[YOUR_SQUID_DEVELOPER_ID].[APP_REGION].squid.cloud/webhooks/[WEBHOOK_ID]
The WebhookRequest object
Every webhook function receives a WebhookRequest object containing the full HTTP context:
@webhook('inspectRequest')
async inspectRequest(request: WebhookRequest): Promise<any> {
console.log('HTTP method:', request.httpMethod);
console.log('Body:', request.body);
console.log('Query params:', request.queryParams);
console.log('Headers:', request.headers);
console.log('Raw body:', request.rawBody);
console.log('Files:', request.files);
return { received: true };
}
| Property | Type | Description |
|---|---|---|
body | any | Parsed request body |
rawBody | string | undefined | Unparsed request body as a string (useful for signature verification) |
queryParams | Record<string, string> | URL query parameters |
headers | Record<string, string> | HTTP headers (keys are lowercase) |
httpMethod | 'post' | 'get' | 'put' | 'delete' | HTTP method of the request |
files | SquidFile[] | undefined | Files uploaded with the request |
Creating responses
There are two ways to return a response from a webhook.
Return a value directly:
Any JSON-serializable return value is sent as the response body with a 200 status code:
@webhook('simpleResponse')
async simpleResponse(request: WebhookRequest): Promise<any> {
return { status: 'ok', timestamp: Date.now() };
}
Use createWebhookResponse for full control:
Set the status code, headers, and body explicitly:
@webhook('customResponse')
async customResponse(request: WebhookRequest): Promise<any> {
const data = await this.processData(request.body);
return this.createWebhookResponse(
{ result: data }, // body
201, // status code
{ 'X-Request-Id': '123' } // custom headers
);
}
Use throwWebhookResponse to return immediately:
Interrupt execution and return a response at any point. This is useful for early validation failures:
@webhook('validateAndProcess')
async validateAndProcess(request: WebhookRequest): Promise<any> {
if (!request.body?.orderId) {
// Immediately returns a 400 response
this.throwWebhookResponse({
body: { error: 'Missing orderId' },
statusCode: 400,
});
}
const result = await this.processOrder(request.body.orderId);
return this.createWebhookResponse({ result }, 200);
}
Supported HTTP methods
Webhooks accept GET, POST, PUT, and DELETE requests. You can check request.httpMethod to handle different methods:
@webhook('resource')
async resource(request: WebhookRequest): Promise<any> {
switch (request.httpMethod) {
case 'get':
return this.getResource(request.queryParams['id']);
case 'post':
return this.createResource(request.body);
case 'delete':
return this.deleteResource(request.queryParams['id']);
default:
return this.createWebhookResponse({ error: 'Method not allowed' }, 405);
}
}
File uploads
Webhooks can receive file uploads. Files are available as SquidFile objects on the request.files array:
@webhook('uploadFile')
async uploadFile(request: WebhookRequest): Promise<any> {
const files = request.files || [];
if (files.length === 0) {
return this.createWebhookResponse({ error: 'No files provided' }, 400);
}
const file = files[0];
console.log('Filename:', file.originalName);
console.log('MIME type:', file.mimetype);
console.log('Size:', file.size);
// Access file content as Uint8Array
const content = new TextDecoder().decode(file.data);
return { filename: file.originalName, size: file.size };
}
Calling webhooks from the Squid client
You can also invoke webhooks from a Squid client using squid.executeWebhook:
const result = await squid.executeWebhook('hello', {
queryParams: { name: 'Squid' },
});
console.log(result); // { message: "Hello, Squid!" }
const result = await squid.executeWebhook('processOrder', {
body: { orderId: 'order-123', items: ['item-1', 'item-2'] },
headers: { 'X-Idempotency-Key': 'unique-key-123' },
});
Error Handling
Throwing errors
If a webhook function throws an unhandled error, Squid returns a 500 response. Use throwWebhookResponse or try/catch to return meaningful error responses:
@webhook('processPayment')
async processPayment(request: WebhookRequest): Promise<any> {
try {
if (!request.body?.amount) {
return this.createWebhookResponse({ error: 'Missing amount' }, 400);
}
const result = await this.chargeCustomer(request.body);
return this.createWebhookResponse({ result }, 200);
} catch (error) {
console.error('Payment processing failed:', error);
return this.createWebhookResponse({ error: 'Internal error' }, 500);
}
}
Verifying webhook signatures
Many external services sign their webhook payloads so you can verify authenticity. Use request.rawBody and request.headers to verify signatures:
import * as crypto from 'crypto';
@webhook('verifiedWebhook')
async verifiedWebhook(request: WebhookRequest): Promise<any> {
const signature = request.headers['x-signature'];
const secret = this.secrets['WEBHOOK_SIGNING_SECRET'] as string;
if (!this.verifySignature(request.rawBody || '', signature, secret)) {
return this.createWebhookResponse({ error: 'Invalid signature' }, 401);
}
// Signature is valid, process the event
return this.handleEvent(request.body);
}
private verifySignature(rawBody: string, signature: string, secret: string): boolean {
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
Common errors
| Error | Cause | Solution |
|---|---|---|
| Webhook not found (404) | Webhook ID doesn't match any @webhook | Check spelling; ensure service is exported |
| 500 response | Unhandled error in webhook function | Add try/catch and return meaningful error responses |
| Empty response body | Function returns undefined | Return a value or use createWebhookResponse |
| Incorrect URL | Wrong app ID, region, or environment | Check .env for correct SQUID_APP_ID and SQUID_REGION |
Rate Limiting
Protect your webhooks from abuse using the @limits decorator. For detailed information on rate and quota limiting, see Rate and quota limiting.
Best Practices
-
Return appropriate status codes. Use
createWebhookResponseto return meaningful HTTP status codes (200 for success, 400 for bad input, 401 for unauthorized, 500 for server errors). -
Verify webhook signatures. When receiving webhooks from external services, always verify the request signature using
request.rawBodyand the service's signing secret. -
Respond quickly. External services often have timeout limits. If processing takes a long time, acknowledge the webhook immediately and process the data asynchronously.
-
Design for idempotency. External services may retry webhook deliveries. Use a unique identifier from the request body to detect and skip duplicate deliveries.
-
Validate input early. Check required fields at the start of your webhook function. Use
throwWebhookResponseto return errors before doing any processing. -
Log incoming requests. Log the webhook event type and key identifiers to help with debugging. Avoid logging sensitive data like full request bodies in production.
Code Examples
Handling a payment event
import { SquidService, webhook, WebhookRequest } from '@squidcloud/backend';
interface PaymentEvent {
type: string;
data: {
object: {
id: string;
customer: string;
amount: number;
status: string;
};
};
}
export class PaymentService extends SquidService {
@webhook('handlePayment')
async handlePayment(request: WebhookRequest<PaymentEvent>): Promise<any> {
const event = request.body;
if (event.type !== 'payment_intent.succeeded') {
return this.createWebhookResponse({ received: true }, 200);
}
const payment = event.data.object;
const payments = this.squid.collection('payments');
await payments.doc(payment.id).insert({
customerId: payment.customer,
amount: payment.amount,
status: payment.status,
createdAt: new Date().toISOString(),
});
console.log(`Recorded payment ${payment.id} for customer ${payment.customer}`);
return this.createWebhookResponse({ received: true }, 200);
}
}
For a complete Stripe webhook tutorial, see Stripe and Squid Webhooks.
Building a REST-style API
import { SquidService, webhook, WebhookRequest } from '@squidcloud/backend';
interface Task {
id: string;
title: string;
completed: boolean;
}
export class TaskApiService extends SquidService {
@webhook('tasks')
async tasks(request: WebhookRequest): Promise<any> {
switch (request.httpMethod) {
case 'get':
return this.listTasks();
case 'post':
return this.createTask(request.body);
default:
return this.createWebhookResponse({ error: 'Method not allowed' }, 405);
}
}
private async listTasks(): Promise<any> {
const tasks = await this.squid.collection<Task>('tasks').query().dereference().snapshot();
return this.createWebhookResponse(tasks, 200);
}
private async createTask(body: any): Promise<any> {
if (!body?.title) {
return this.createWebhookResponse({ error: 'Missing title' }, 400);
}
const taskId = crypto.randomUUID();
const task: Task = { id: taskId, title: body.title, completed: false };
await this.squid.collection<Task>('tasks').doc(taskId).insert(task);
return this.createWebhookResponse(task, 201);
}
}
Receiving and processing file uploads
import { SquidService, webhook, WebhookRequest } from '@squidcloud/backend';
export class FileWebhookService extends SquidService {
@webhook('uploadDocument')
async uploadDocument(request: WebhookRequest): Promise<any> {
const files = request.files || [];
if (files.length === 0) {
return this.createWebhookResponse({ error: 'No files provided' }, 400);
}
const results: Array<{ name: string; docId?: string; size?: number; error?: string }> = [];
for (const file of files) {
// Validate file type
if (!file.mimetype.startsWith('text/') && !file.mimetype.includes('pdf')) {
results.push({ name: file.originalName, error: 'Unsupported file type' });
continue;
}
// Store metadata
const docId = crypto.randomUUID();
await this.squid.collection('documents').doc(docId).insert({
name: file.originalName,
mimeType: file.mimetype,
size: file.size,
uploadedAt: new Date().toISOString(),
});
results.push({ name: file.originalName, docId, size: file.size });
}
return this.createWebhookResponse({ uploaded: results }, 200);
}
}
See Also
- Executables - Call backend functions from the client
- Triggers - React to database changes
- Schedulers - Run code on a schedule
- Rate and quota limiting - Protect your backend functions
- Stripe Webhooks Tutorial - End-to-end Stripe integration