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

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 します。

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

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 を使用

仕組み​

  1. SquidService を拡張する class 内で、method を @webhook('webhookId') で decorate します
  2. Squid が deploy 時に webhook を検出・登録します
  3. Squid が webhook を HTTP endpoint として公開します
  4. external service が endpoint URL に request を送信します
  5. function が request を受け取り、response を返します

クイックスタート​

前提条件​

  • squid init で初期化された Squid backend project
  • NPM からインストールされた @squidcloud/backend package

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

SquidService を拡張する service class を作成し、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);
}
}

ステップ 2: service を export する​

service が service index file から export されていることを確認します。

Backend code
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 を生成できます。

Client code
// 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();

WebhookRequest object​

すべての webhook function は、完全な HTTP context を含む WebhookRequest object を受け取ります。

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 };
}
PropertyType説明
bodyanyparse 済み request body
rawBodystring | undefinedstring としての unparsed request body(signature verification に便利)
queryParamsRecord<string, string>URL query parameter
headersRecord<string, string>HTTP header(key は lowercase)
httpMethod'post' | 'get' | 'put' | 'delete'request の HTTP method
filesSquidFile[] | undefinedrequest とともに upload された file

Response の作成​

webhook から response を返す方法は 2 つあります。

Value を直接返す:

JSON-serializable な return value は、200 status code の response body として送信されます。

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

完全な制御には createWebhookResponse を使用する:

status code、header、body を明示的に設定します。

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

直ちに return するには throwWebhookResponse を使用する:

execution を中断し、任意の時点で response を返します。early validation failure に役立ちます。

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 method​

Webhooks は GET、POST、PUT、DELETE request を受け入れます。異なる method を処理するには 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);
}
}

File upload​

Webhooks は file upload を受信できます。file は request.files array 内の SquidFile object として利用できます。

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 を invoke することもできます。

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​

Error の throw​

webhook function が unhandled error を throw すると、Squid は 500 response を返します。意味のある error response を返すには、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 signature の検証​

多くの external service は webhook payload に signature を付与するため、authenticity を検証できます。signature の検証には request.rawBody と request.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));
}

一般的な error​

Error原因解決策
Webhook not found (404)Webhook ID がどの @webhook とも一致しないspelling を確認し、service が export されていることを確認する
500 responsewebhook function 内の unhandled errortry/catch を追加し、意味のある error response を返す
Empty response bodyfunction が undefined を返すvalue を返すか、createWebhookResponse を使用する
Incorrect URLapp ID、region、または environment が誤っている.env で正しい SQUID_APP_ID と SQUID_REGION を確認する

Rate Limiting​

@limits decorator を使用して webhook を不正利用から保護します。rate と quota limiting の詳細については、Rate and quota limitingを参照してください。

ベストプラクティス​

  1. 適切な status code を返す。 createWebhookResponse を使用して、意味のある HTTP status code を返します(成功は 200、不正な input は 400、unauthorized は 401、server error は 500)。

  2. Webhook signature を検証する。 external service から webhook を受信する場合、request.rawBody と service の signing secret を使用して常に request signature を検証します。

  3. 迅速に response を返す。 external service には timeout limit があることがよくあります。processing に時間がかかる場合、webhook をすぐに acknowledge して、data を asynchronous に処理します。

  4. idempotency を考慮して設計する。 external service が webhook delivery を retry する可能性があります。request body の unique identifier を使用して、duplicate delivery を検出し skip します。

  5. input を早期に validate する。 webhook function の開始時に required field を確認します。processing を行う前に error を返すには、throwWebhookResponse を使用します。

  6. incoming request を log に記録する。 debug を支援するため、webhook event type と主要 identifier を log に記録します。production では完全な request body などの sensitive data を log に記録しないでください。

コード例​

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

完全な Stripe webhook tutorial については、Stripe and Squid Webhooksを参照してください。

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

File upload の受信と処理​

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

関連項目​