Photon methods can receive HTTP webhook callbacks from external services. Webhook methods are exposed as POST endpoints on the daemon.
POST /webhook/{photonName}/{method}
For example, a photon named forms with a method handleSubmission is reachable at:
POST /webhook/forms/handleSubmission
Any method starting with handle is automatically treated as a webhook handler:
async handleStripeEvent(params: { type: string; data: object }) {
// Automatically exposed as POST /webhook/{photon}/handleStripeEvent
}Explicitly mark a method as a webhook handler:
/**
* Process form submission
* @webhook
*/
async processForm(params: { email: string; name: string }) {
// Exposed as POST /webhook/{photon}/processForm
}With a custom path:
/**
* GitHub push events
* @webhook github/push
*/
async onPush(params: { ref: string; commits: object[] }) {
// Exposed at the custom path
}The @webhook tag takes precedence over the handle* prefix when both are present.
Webhook methods receive HTTP context via the _webhook metadata field on the params object:
interface WebhookMetadata {
method: string; // HTTP method (always 'POST')
headers: Record<string, string>; // Request headers
query: Record<string, string>; // URL query parameters
timestamp: number; // Unix timestamp of request receipt
}Access it in your handler:
async handleEvent(params: { type: string; _webhook: WebhookMetadata }) {
const signature = params._webhook.headers['x-hub-signature-256'];
const receivedAt = new Date(params._webhook.timestamp);
// ...
}Set the PHOTON_WEBHOOK_SECRET environment variable to require authentication on all webhook endpoints:
export PHOTON_WEBHOOK_SECRET=whsec_abc123...Callers must include the secret in the X-Webhook-Secret header:
curl -X POST https://example.com/webhook/forms/handleSubmission \
-H "X-Webhook-Secret: whsec_abc123..." \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "name": "Alice"}'Requests without a valid secret receive a 401 Unauthorized response.
import { Photon } from '@portel/photon-core';
class FormHandler extends Photon {
/**
* Handle contact form submission
* @webhook
* @param email Submitter email
* @param name Submitter name
* @param message Form message
*/
async handleContactForm(params: {
email: string;
name: string;
message: string;
_webhook: { method: string; headers: Record<string, string>; query: Record<string, string>; timestamp: number };
}) {
// Verify the request came recently
const age = Date.now() - params._webhook.timestamp;
if (age > 60_000) {
throw new Error('Request too old');
}
await this.saveSubmission({
email: params.email,
name: params.name,
message: params.message,
receivedAt: new Date(params._webhook.timestamp),
});
return { success: true };
}
}