メインコンテンツまでスキップ

Webhooks

外部サービスが呼び出せる HTTP エンドポイントを公開します。

Webhooks を使う理由

アプリケーションが外部サービスから HTTP リクエストを受け取る必要があるケースがあります。たとえば、決済プロバイダーが請求の完了を通知する、ソースコード管理プラットフォームが新しいコミットを報告する、監視ツールがアラートを送信する、といった場合です。

webhooks では、関数にデコレーターを付けてデプロイするだけです。

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);
}

ルーティングは不要。Express サーバーも不要。URL の背後にある関数だけで完結します。

概要

Webhooks は、HTTP エンドポイントとして公開されるバックエンド関数です。外部サービスはこれらのエンドポイントへリクエストを送信し、あなたの関数が受信データを処理します。Squid が URL ルーティング、リクエストのパース、レスポンスのシリアライズを担当します。

Webhooks を使うべきとき

ユースケース推奨
外部サービスから HTTP リクエストを受け取る✅ Webhook
Squid client から関数を呼び出すExecutables を使用
データベースの変更に反応するTriggers を使用
スケジュールでコードを実行するSchedulers を使用

仕組み

  1. SquidService を拡張したクラス内で、メソッドに @webhook('webhookId') を付ける
  2. Squid がデプロイ時に webhook を検出して登録する
  3. Squid が webhook を HTTP エンドポイントとして公開する
  4. 外部サービスがエンドポイント URL にリクエストを送信する
  5. 関数がリクエストを受け取り、レスポンスを返す

クイックスタート

前提条件

  • squid init で初期化された Squid backend プロジェクト
  • @squidcloud/backend パッケージがNPMからインストール済み

ステップ 1: webhook を作成する

SquidService を拡張する service クラスを作成し、webhook 関数を追加します。

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);
}
}

ステップ 2: service をエクスポートする

service index ファイルから service がエクスポートされていることを確認します。

Backend code
export * from './example-service';

ステップ 3: backend を起動またはデプロイする

ローカル開発では、Squid CLI を使って backend をローカルで起動します。

squid start

クラウドにデプロイする場合は、deploying your backend を参照してください。

ステップ 4: webhook をテストする

デプロイ後、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"

レスポンス:

{ "message": "Hello, Squid!" }

コアコンセプト

Webhook URL フォーマット

デプロイ後、各 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]

WebhookRequest オブジェクト

すべての webhook 関数は、HTTP の全コンテキストを含む WebhookRequest オブジェクトを受け取ります。

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 };
}
プロパティ説明
bodyanyパース済みのリクエストボディ
rawBodystring | undefined未パースのリクエストボディ(文字列)。署名検証に有用
queryParamsRecord<string, string>URL クエリパラメータ
headersRecord<string, string>HTTP ヘッダー(キーは小文字)
httpMethod'post' | 'get' | 'put' | 'delete'リクエストの HTTP メソッド
filesSquidFile[] | undefinedリクエストとともにアップロードされたファイル

レスポンスの作成

webhook からレスポンスを返す方法は 2 つあります。

値を直接 return する:

JSON シリアライズ可能な戻り値は、ステータスコード 200 とともにレスポンスボディとして送信されます。

Backend code
@webhook('simpleResponse')
async simpleResponse(request: WebhookRequest): Promise<any> {
return { status: 'ok', timestamp: Date.now() };
}

createWebhookResponse を使って完全に制御する:

ステータスコード、ヘッダー、ボディを明示的に設定します。

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
);
}

throwWebhookResponse を使って即時に返す:

任意の時点で実行を中断し、レスポンスを返します。早期バリデーション失敗に便利です。

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);
}

対応する HTTP メソッド

Webhooks は GETPOSTPUTDELETE リクエストを受け付けます。request.httpMethod を確認して、異なるメソッドを扱えます。

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);
}
}

ファイルアップロード

Webhooks はファイルアップロードを受け取れます。ファイルは request.files 配列上の SquidFile オブジェクトとして利用できます。

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 };
}

Squid client から webhooks を呼び出す

squid.executeWebhook を使って、Squid client から webhook を呼び出すこともできます。

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' },
});

エラーハンドリング

エラーを投げる

webhook 関数が未処理のエラーを throw すると、Squid は 500 レスポンスを返します。throwWebhookResponse または try/catch を使って、意味のあるエラーレスポンスを返してください。

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);
}
}

webhook 署名の検証

多くの外部サービスは webhook ペイロードに署名を付け、真正性を検証できるようにしています。request.rawBodyrequest.headers を使って署名を検証します。

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));
}

よくあるエラー

エラー原因解決策
Webhook not found (404)Webhook ID がどの @webhook とも一致しないスペルを確認し、service がエクスポートされていることを確認する
500 responsewebhook 関数内の未処理エラーtry/catch を追加し、意味のあるエラーレスポンスを返す
Empty response body関数が undefined を返している値を返す、または createWebhookResponse を使用する
Incorrect URLapp ID / region / environment が誤っている.envSQUID_APP_IDSQUID_REGION が正しいか確認する

レート制限 (Rate Limiting)

@limits デコレーターを使って、webhooks を不正利用から保護します。レート制限とクォータ制限の詳細は、Rate and quota limiting を参照してください。

ベストプラクティス

  1. 適切なステータスコードを返す。 createWebhookResponse を使い、意味のある HTTP ステータスコード(成功は 200、不正な入力は 400、未認証は 401、サーバーエラーは 500)を返します。

  2. webhook 署名を検証する。 外部サービスから webhooks を受信する際は、request.rawBody とサービスの signing secret を使って必ずリクエスト署名を検証してください。

  3. 素早く応答する。 外部サービスにはタイムアウト制限があることが多いです。処理に時間がかかる場合は、webhook をすぐに acknowledge し、データ処理は非同期に行ってください。

  4. 冪等性 (idempotency) を前提に設計する。 外部サービスは webhook 配信をリトライすることがあります。リクエストボディ内の一意な識別子を使って重複配信を検出し、スキップしてください。

  5. 入力を早期にバリデーションする。 webhook 関数の冒頭で必須フィールドをチェックします。throwWebhookResponse を使って処理前にエラーを返してください。

  6. 受信リクエストをログに残す。 デバッグのために webhook の event type と主要な識別子をログに残します。本番環境ではリクエストボディ全体のような機微なデータをログに出さないでください。

コード例

決済イベントを処理する

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);
}
}

完全な Stripe webhook チュートリアルは、Stripe and Squid Webhooks を参照してください。

REST スタイルの 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);
}
}

ファイルアップロードを受信して処理する

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)