Webhook Integration
Event Types
Aiffinity sends webhook events to your configured endpoint when significant platform events occur. Subscribe to specific event types when configuring your webhook profile, or use ["*"] to receive all events.
| Event Type | Category | Description |
|---|---|---|
package.submitted | Package | A package version has been submitted for review |
package.published | Package | A package version has been approved and published to the catalog |
package.suspended | Package | A package has been suspended due to policy violation or inactivity |
install.created | Install | A user has installed your package |
install.removed | Install | A user has uninstalled your package |
capability.invoked | Capability | A capability was successfully invoked for a user |
capability.failed | Capability | A capability invocation failed (timeout, error, health check failure) |
Payload Schemas
Every webhook delivery is an HTTP POST with a JSON body. All payloads share a common envelope structure with event-specific data in the data field.
Common Envelope
{
"id": "evt_01J8XKWM3N4QR5STVYZ2BCDEFG",
"type": "package.published",
"created_at": "2026-04-04T10:30:00Z",
"app_id": "app_01J8XKWM3N4QR5STVYZ2BCDEFG",
"idempotency_key": "idem_abc123def456",
"data": { ... }
}
package package.submitted
Fired when a package version is submitted for review.
{
"id": "evt_pkg_sub_001",
"type": "package.submitted",
"created_at": "2026-04-04T10:30:00Z",
"app_id": "app_01J8XKWM3N4QR5STVYZ2BCDEFG",
"idempotency_key": "idem_pkg_sub_001",
"data": {
"package_id": "pkg_acme_weather",
"version_id": "ver_1_0_0",
"version": "1.0.0",
"name": "Acme Weather Widgets",
"risk_tier": "data_only",
"submitted_by": "[email protected]"
}
}
package package.published
Fired when a package version passes review and is published to the catalog.
{
"id": "evt_pkg_pub_001",
"type": "package.published",
"created_at": "2026-04-04T11:00:00Z",
"app_id": "app_01J8XKWM3N4QR5STVYZ2BCDEFG",
"idempotency_key": "idem_pkg_pub_001",
"data": {
"package_id": "pkg_acme_weather",
"version_id": "ver_1_0_0",
"version": "1.0.0",
"name": "Acme Weather Widgets",
"catalog_url": "https://aiffinity.me/providers/acme-weather-widgets"
}
}
package package.suspended
Fired when a package is suspended. Includes the reason and any remediation steps.
{
"id": "evt_pkg_sus_001",
"type": "package.suspended",
"created_at": "2026-04-04T15:00:00Z",
"app_id": "app_01J8XKWM3N4QR5STVYZ2BCDEFG",
"idempotency_key": "idem_pkg_sus_001",
"data": {
"package_id": "pkg_acme_weather",
"reason": "health_check_failures",
"message": "Health check endpoint returned 503 for 72 consecutive hours",
"suspended_at": "2026-04-04T15:00:00Z",
"remediation": "Restore your health check endpoint and contact support to reinstate."
}
}
install install.created
Fired when a user installs your package.
{
"id": "evt_inst_cre_001",
"type": "install.created",
"created_at": "2026-04-04T12:00:00Z",
"app_id": "app_01J8XKWM3N4QR5STVYZ2BCDEFG",
"idempotency_key": "idem_inst_cre_001",
"data": {
"install_id": "inst_usr_abc123",
"user_id": "usr_def456",
"package_id": "pkg_acme_weather",
"version": "1.0.0",
"scopes_granted": ["weather:read", "forecast:read"]
}
}
install install.removed
Fired when a user uninstalls your package. Clean up any user-specific resources.
{
"id": "evt_inst_rem_001",
"type": "install.removed",
"created_at": "2026-04-04T13:00:00Z",
"app_id": "app_01J8XKWM3N4QR5STVYZ2BCDEFG",
"idempotency_key": "idem_inst_rem_001",
"data": {
"install_id": "inst_usr_abc123",
"user_id": "usr_def456",
"package_id": "pkg_acme_weather",
"reason": "user_initiated"
}
}
capability capability.invoked
Fired after a successful capability execution. Useful for analytics and usage tracking.
{
"id": "evt_cap_inv_001",
"type": "capability.invoked",
"created_at": "2026-04-04T14:00:00Z",
"app_id": "app_01J8XKWM3N4QR5STVYZ2BCDEFG",
"idempotency_key": "idem_cap_inv_001",
"data": {
"capability_name": "current_weather",
"mode": "state",
"user_id": "usr_def456",
"install_id": "inst_usr_abc123",
"request_id": "req_abc123",
"duration_ms": 245,
"status": "ok"
}
}
capability capability.failed
Fired when a capability invocation fails. Includes the error code and whether retries were exhausted.
{
"id": "evt_cap_fail_001",
"type": "capability.failed",
"created_at": "2026-04-04T14:05:00Z",
"app_id": "app_01J8XKWM3N4QR5STVYZ2BCDEFG",
"idempotency_key": "idem_cap_fail_001",
"data": {
"capability_name": "current_weather",
"mode": "state",
"user_id": "usr_def456",
"install_id": "inst_usr_abc123",
"request_id": "req_def456",
"error_code": "UPSTREAM_UNAVAILABLE",
"error_message": "Weather API returned 503",
"retries_exhausted": true,
"duration_ms": 10042
}
}
Signature Verification
Every webhook delivery includes an X-Aiffinity-Signature header containing an HMAC-SHA256 signature of the raw request body. You must verify this signature before processing any event to ensure the request originated from Aiffinity and was not tampered with.
How it works
- Aiffinity computes
HMAC-SHA256(webhook_secret, raw_request_body) - The hex-encoded digest is sent as:
X-Aiffinity-Signature: sha256=<hex_digest> - Your server re-computes the HMAC using the same secret and compares it to the header value
- Use a constant-time comparison to prevent timing attacks
TypeScript Implementation
import { createHmac, timingSafeEqual } from 'node:crypto';
import type { IncomingMessage } from 'node:http';
const WEBHOOK_SECRET = process.env.AIFFINITY_WEBHOOK_SECRET!;
function verifySignature(rawBody: Buffer, signatureHeader: string): boolean {
// The header format is: sha256=<hex_digest>
const expectedPrefix = 'sha256=';
if (!signatureHeader.startsWith(expectedPrefix)) {
return false;
}
const receivedDigest = signatureHeader.slice(expectedPrefix.length);
const computedDigest = createHmac('sha256', WEBHOOK_SECRET)
.update(rawBody)
.digest('hex');
// Constant-time comparison to prevent timing attacks
const receivedBuffer = Buffer.from(receivedDigest, 'hex');
const computedBuffer = Buffer.from(computedDigest, 'hex');
if (receivedBuffer.length !== computedBuffer.length) {
return false;
}
return timingSafeEqual(receivedBuffer, computedBuffer);
}
// Express middleware example
import express from 'express';
const app = express();
app.post(
'/webhooks/aiffinity',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-aiffinity-signature'] as string;
if (!signature || !verifySignature(req.body, signature)) {
console.error('Webhook signature verification failed');
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body.toString());
// Process the event
switch (event.type) {
case 'install.created':
console.log(`User ${event.data.user_id} installed package`);
break;
case 'install.removed':
console.log(`User ${event.data.user_id} removed package`);
break;
case 'capability.failed':
console.error(`Capability ${event.data.capability_name} failed: ${event.data.error_code}`);
break;
default:
console.log(`Received event: ${event.type}`);
}
// Respond with 200 to acknowledge receipt
res.status(200).json({ received: true });
}
);
Important: You must parse the raw request body for signature verification. If you use express.json() before verifying, the re-serialized JSON may differ from the original payload, causing verification to fail. Use express.raw({ type: 'application/json' }) as shown above.
Retry Policy
If your endpoint does not respond with a 2xx status code within the configured timeout (default: 5 seconds), Aiffinity retries the delivery with exponential backoff.
Summary:
| Parameter | Value |
|---|---|
| Maximum retries | 5 |
| Backoff strategy | Exponential with jitter |
| Maximum elapsed time | 24 hours |
| Request timeout | 5 seconds (configurable up to 30s) |
| Expected response | Any 2xx status code |
After all retries are exhausted, the failed delivery appears in the webhook delivery log in your developer console. You can manually replay any failed delivery from the console UI.
Testing Webhooks
The Aiffinity developer console provides tools for testing your webhook integration before going live.
Console UI
Navigate to your app's Webhooks tab at dev.aiffinity.me to:
- Send test events — Select any event type and fire a test delivery with sample data to your configured endpoint
- View delivery log — Inspect request/response pairs for every delivery attempt, including headers, body, status code, and timing
- Replay failed deliveries — Re-send any failed event with a single click
- Inspect signatures — View the computed signature alongside the raw body for debugging verification issues
CLI Testing
# Send a test webhook event
npx @aiffinity/provider-platform-sdk webhook test \
--app-id $APP_ID \
--event install.created
# List recent webhook deliveries
npx @aiffinity/provider-platform-sdk webhook deliveries \
--app-id $APP_ID \
--limit 20
# Replay a failed delivery
npx @aiffinity/provider-platform-sdk webhook replay \
--app-id $APP_ID \
--delivery-id del_abc123
Local Development
For local development, use a tunnel service to expose your local server:
# Start your local webhook handler
node handler.js # listening on port 3000
# Expose it via a tunnel (e.g. ngrok, cloudflared)
ngrok http 3000
# Update your app's webhook URL to the tunnel URL
curl -X PATCH https://api.aiffinity.me/v1/providers/runtime/apps/$APP_ID \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "webhook_url": "https://abc123.ngrok.io/webhooks/aiffinity" }'
Best Practices
Idempotency
Every webhook event includes an idempotency_key field. Store processed keys and check for duplicates before processing. Due to retries and at-least-once delivery, your endpoint may receive the same event more than once.
// Example: check idempotency before processing
async function handleWebhook(event: WebhookEvent): Promise<void> {
const alreadyProcessed = await db.webhookEvents.findByIdempotencyKey(
event.idempotency_key
);
if (alreadyProcessed) {
console.log(`Skipping duplicate event: ${event.idempotency_key}`);
return;
}
// Process the event
await processEvent(event);
// Mark as processed
await db.webhookEvents.insert({
idempotency_key: event.idempotency_key,
event_type: event.type,
processed_at: new Date(),
});
}
Timeout Handling
Your endpoint must respond within the configured timeout (default: 5 seconds). If your event processing takes longer:
- Acknowledge the webhook immediately with
202 Accepted - Queue the event for asynchronous processing
- Do not block the HTTP response on downstream work
Response Codes
| Status Code | Meaning | Aiffinity Behavior |
|---|---|---|
200 / 202 | Success | Event marked as delivered |
301 / 302 | Redirect | Not followed; treated as failure and retried |
400 | Bad request | No retry (your endpoint explicitly rejected the event) |
401 / 403 | Auth error | No retry (signature verification should handle this) |
404 | Not found | Retried (endpoint may be temporarily misconfigured) |
429 | Rate limited | Retried with backoff, respecting Retry-After header |
500 / 502 / 503 | Server error | Retried with exponential backoff |
| Timeout | No response in time | Retried with exponential backoff |
Security Recommendations
- Always verify signatures — Never process a webhook without validating the
X-Aiffinity-Signatureheader - Use HTTPS — Webhook URLs must use HTTPS with a valid TLS certificate
- Rotate secrets periodically — Rotate your webhook signing secret via the developer console or API every 90 days
- Restrict IP ranges — If your infrastructure supports it, allowlist Aiffinity's webhook delivery IP ranges (published at
https://api.aiffinity.me/.well-known/webhook-ips) - Log deliveries — Keep a log of all received webhook events for debugging and audit purposes