Webhooks
external service が呼び出せる HTTP endpoint を公開します。
Webhooks を使用する理由
application が external service から HTTP request を受信する必要があります。たとえば、payment provider からの charge 完了通知、source control platform からの新規 commit の報告、monitoring tool からの alert 送信などです。
webhooks では、function を decorate して 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);
}
route は不要です。Express server も不要です。URL の背後にある function だけで実現できます。
概要
Webhooks は HTTP endpoint として公開される backend function です。external service はこれらの endpoint に request を送信し、function が incoming data を処理します。Squid は URL routing、request parsing、response serialization を処理します。
Webhooks を使用する場合
| ユースケース | 推奨 |
|---|---|
| external service から HTTP request を受信する | ✅ Webhook |
| Squid client から function を呼び出す | Executables を使用 |
| database の変更に反応する | Triggers を使用 |
| schedule に従って code を実行する | Schedulers を使用 |
仕組み
SquidServiceを拡張する class 内で、method を@webhook('webhookId')で decorate します- Squid が deploy 時に webhook を検出・登録します
- Squid が webhook を HTTP endpoint として公開します
- external service が endpoint URL に request を送信します
- function が request を受け取り、response を返します
クイックスタート
前提条件
squid initで初期化された Squid backend project- NPM からインストールされた
@squidcloud/backendpackage
ステップ 1: webhook を作成する
SquidService を拡張する service class を作成し、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);
}
}
ステップ 2: service を export する
service が service index file から export されていることを確認します。
export * from './example-service';
ステップ 3: backend を開始または deploy する
ローカル開発では、Squid CLI を使用して backend をローカルで実行します。
squid start
cloud に deploy するには、backend の deployを参照してください。
ステップ 4: webhook を test する
deploy 後、webhook は次の URL で利用できます。
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"
response:
{ "message": "Hello, Squid!" }
コアコンセプト
Webhook URL format
deploy 後、各 webhook は ID に基づく URL でアクセスできます。
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]
URL を手動で構築する代わりに、Squid client で getWebhookUrl() を呼び出します。client 自身の configuration から region、environment、developer ID を導出し、webhook ID を URL-encode するため、同じ code で production、dev、local development それぞれに正しい URL を生成できます。
- TypeScript
- Python
// Full URL for a specific webhook:
const url = squid.getWebhookUrl('stripe-events');
// Base .../webhooks URL, useful as a prefix for external callers:
const base = squid.getWebhookUrl();
# Full URL for a specific webhook (the webhook ID is required in Python):
url = squid.get_webhook_url('stripe-events')
WebhookRequest object
すべての webhook function は、完全な HTTP context を含む WebhookRequest object を受け取ります。
@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 | 説明 |
|---|---|---|
body | any | parse 済み request body |
rawBody | string | undefined | string としての unparsed request body(signature verification に便利) |
queryParams | Record<string, string> | URL query parameter |
headers | Record<string, string> | HTTP header(key は lowercase) |
httpMethod | 'post' | 'get' | 'put' | 'delete' | request の HTTP method |
files | SquidFile[] | undefined | request とともに upload された file |
Response の作成
webhook から response を返す方法は 2 つあります。
Value を直接返す:
JSON-serializable な return value は、200 status code の response body として送信されます。
@webhook('simpleResponse')
async simpleResponse(request: WebhookRequest): Promise<any> {
return { status: 'ok', timestamp: Date.now() };
}
完全な制御には createWebhookResponse を使用する:
status code、header、body を明示的に設定します。
@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
);
}
直ちに return するには throwWebhookResponse を使用する:
execution を中断し、任意の時点で response を返します。early validation failure に役立ちます。
@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);
}
サポートされる HTTP method
Webhooks は GET、POST、PUT、DELETE request を受け入れます。異なる method を処理するには request.httpMethod を確認します。
@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 upload
Webhooks は file upload を受信できます。file は request.files array 内の SquidFile object として利用できます。
@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 };
}
Squid client から webhook を呼び出す
squid.executeWebhook を使用して、Squid client から webhook を invoke することもできます。
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
Error の throw
webhook function が unhandled error を throw すると、Squid は 500 response を返します。意味のある error response を返すには、throwWebhookResponse または try/catch を使用します。
@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);
}
}
Webhook signature の検証
多くの external service は webhook payload に signature を付与するため、authenticity を検証できます。signature の検証には request.rawBody と request.headers を使用します。
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));
}
一般的な error
| Error | 原因 | 解決策 |
|---|---|---|
| Webhook not found (404) | Webhook ID がどの @webhook とも一致しない | spelling を確認し、service が export されていることを確認する |
| 500 response | webhook function 内の unhandled error | try/catch を追加し、意味のある error response を返す |
| Empty response body | function が undefined を返す | value を返すか、createWebhookResponse を使用する |
| Incorrect URL | app ID、region、または environment が誤っている | .env で正しい SQUID_APP_ID と SQUID_REGION を確認する |
Rate Limiting
@limits decorator を使用して webhook を不正利用から保護します。rate と quota limiting の詳細については、Rate and quota limitingを参照してください。
ベストプラクティス
-
適切な status code を返す。
createWebhookResponseを使用して、意味のある HTTP status code を返します(成功は 200、不正な input は 400、unauthorized は 401、server error は 500)。 -
Webhook signature を検証する。 external service から webhook を受信する場合、
request.rawBodyと service の signing secret を使用して常に request signature を検証します。 -
迅速に response を返す。 external service には timeout limit があることがよくあります。processing に時間がかかる場合、webhook をすぐに acknowledge して、data を asynchronous に処理します。
-
idempotency を考慮して設計する。 external service が webhook delivery を retry する可能性があります。request body の unique identifier を使用して、duplicate delivery を検出し skip します。
-
input を早期に validate する。 webhook function の開始時に required field を確認します。processing を行う前に error を返すには、
throwWebhookResponseを使用します。 -
incoming request を log に記録する。 debug を支援するため、webhook event type と主要 identifier を log に記録します。production では完全な request body などの sensitive data を log に記録しないでください。
コード例
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);
}
}
完全な Stripe webhook tutorial については、Stripe and Squid Webhooksを参照してください。
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);
}
}
File upload の受信と処理
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);
}
}
関連項目
- Executables - client から backend function を呼び出す
- Triggers - database の変更に反応する
- Schedulers - schedule に従って code を実行する
- Rate and quota limiting - backend function を保護する
- Stripe Webhooks Tutorial - end-to-end の Stripe integration