Skip to main content

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:

Backend code
// 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 CaseRecommendation
Receive HTTP requests from external services✅ Webhook
Call a function from the Squid clientUse Executables
React to database changesUse Triggers
Run code on a scheduleUse Schedulers

How it works

  1. You decorate a method with @webhook('webhookId') in a class that extends SquidService
  2. Squid discovers and registers the webhook at deploy time
  3. Squid exposes the webhook as an HTTP endpoint
  4. External services send requests to the endpoint URL
  5. Your function receives the request and returns a response

Quick Start

Prerequisites

  • A Squid backend project initialized with squid init-backend
  • The @squidcloud/backend package installed

Step 1: Create a webhook

Create a service class that extends SquidService and add your webhook function:

Backend code
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:

Backend code
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:

Backend code
@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 };
}
PropertyTypeDescription
bodyanyParsed request body
rawBodystring | undefinedUnparsed request body as a string (useful for signature verification)
queryParamsRecord<string, string>URL query parameters
headersRecord<string, string>HTTP headers (keys are lowercase)
httpMethod'post' | 'get' | 'put' | 'delete'HTTP method of the request
filesSquidFile[] | undefinedFiles 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:

Backend 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:

Backend code
@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:

Backend code
@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:

Backend code
@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:

Backend code
@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:

Client code
const result = await squid.executeWebhook('hello', {
queryParams: { name: 'Squid' },
});
console.log(result); // { message: "Hello, Squid!" }
Client code
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:

Backend code
@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:

Backend code
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

ErrorCauseSolution
Webhook not found (404)Webhook ID doesn't match any @webhookCheck spelling; ensure service is exported
500 responseUnhandled error in webhook functionAdd try/catch and return meaningful error responses
Empty response bodyFunction returns undefinedReturn a value or use createWebhookResponse
Incorrect URLWrong app ID, region, or environmentCheck .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

  1. Return appropriate status codes. Use createWebhookResponse to return meaningful HTTP status codes (200 for success, 400 for bad input, 401 for unauthorized, 500 for server errors).

  2. Verify webhook signatures. When receiving webhooks from external services, always verify the request signature using request.rawBody and the service's signing secret.

  3. Respond quickly. External services often have timeout limits. If processing takes a long time, acknowledge the webhook immediately and process the data asynchronously.

  4. Design for idempotency. External services may retry webhook deliveries. Use a unique identifier from the request body to detect and skip duplicate deliveries.

  5. Validate input early. Check required fields at the start of your webhook function. Use throwWebhookResponse to return errors before doing any processing.

  6. 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

Backend code
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

Backend code
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

Backend code
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