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

Webhook

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

Webhook を使う理由

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

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

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 の背後にある関数だけ。

概要

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

Webhook を使うべきとき

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

仕組み

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

クイックスタート

前提条件

  • squid init-backend で初期化された Squid backend プロジェクト
  • @squidcloud/backend パッケージがインストールされていること

Step 1: Webhook を作成する

SquidService を継承するサービスクラスを作成し、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);
}
}

Step 2: サービスを export する

サービスが service の index ファイルから export されていることを確認します。

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

Step 3: バックエンドを起動またはデプロイする

ローカル開発では、Squid CLI を使ってバックエンドをローカル実行します。

squid start

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

Step 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 にシリアライズ可能な return 値は、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 メソッド

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

ファイルアップロード

Webhook はファイルアップロードも受信できます。ファイルは 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 から Webhook を呼び出す

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

エラーハンドリング

エラーを throw する

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 とも一致しないスペルを確認し、サービスが export されていることを確認
500 responsewebhook 関数内の未処理エラーtry/catch を追加し、意味のあるエラーレスポンスを返す
Empty response body関数が undefined を返している値を返すか createWebhookResponse を使う
Incorrect URLapp ID、region、environment が間違っている.envSQUID_APP_IDSQUID_REGION が正しいか確認

レート制限(Rate Limiting)

@limits デコレータを使って、Webhook を悪用から保護できます。レートおよびクォータ制限の詳細は、Rate and quota limiting を参照してください。

ベストプラクティス

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

  2. Webhook 署名を検証する。 外部サービスから webhook を受け取る場合、request.rawBody とサービスの signing secret を使って、常にリクエスト署名を検証してください。

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

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

  5. 入力を早期に検証する。 webhook 関数の冒頭で必須フィールドを確認してください。throwWebhookResponse を使い、処理を始める前にエラーを返せます。

  6. 受信リクエストをログに残す。 デバッグに役立つよう、Webhook のイベントタイプや主要な識別子をログに記録してください。production ではリクエストボディ全文などの機微情報をログに出さないようにしてください。

コード例

決済イベントを処理する

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)