Webhooks
Webhooks let your server receive real‑time notifications when important events happen in Passage. Register a publicly reachable HTTPS endpoint for your webhooks and Passage will POST a JSON payload for every event.
Security
- Use HTTPS for your endpoint.
- Optional: Verify idempotency using the
idfield of each webhook to avoid duplicate processing
Events
Every time a connection changes status, passage will send your service a webhook. Connection statuses are listed below.
type ConnectionStatus =
"pending" |
"connecting" |
"connected" |
"canceled" |
"rejected" |
"failed" |
"data_processing" |
"data_available" |
"data_partially_available" |
"inactive"
If you just care about receiving data, you will want to listen for the data_available status.
Top-level payload
Each webhook shares a common envelope:
{
"id": "evt_123",
"timestamp": "2025-01-01T00:00:00.000Z",
"type": "Connection.Updated",
"data": { /* event-specific object */ }
}
id(string): Unique event id. Treat as idempotency key.timestamp(ISO-8601 string): When the event occurred.type(string): One of the event types above.data(object): Event-specific payload shown below.
Connection events
The body of the webhook will contain a resources section that will tell you the status of each resource and a url that you can use to fetch that resource
Example full payload:
{
"id": "evt_9b1f",
"timestamp": "2025-01-01T12:34:56.000Z",
"type": "Connection.Updated",
"data": {
"connectionId": "123",
"status": "data_available",
"resources": [
{
"type": "trip",
"url": "https://api.getpassage.ai/connections/123/resources/trip",
"status": "data_available",
"operation": "read",
"startedAt": "2025-01-01T00:00:00.000Z",
"completedAt": "2025-01-01T00:01:00.000Z"
},
{
"type": "account_info",
"url": "https://api.getpassage.ai/connections/123/resources/account_info",
"status": "data_available",
"operation": "read",
"startedAt": "2025-01-01T00:00:00.000Z",
"completedAt": "2025-01-01T00:01:00.000Z"
}
]
}
}
Handling webhooks
Pseudo-code example in Node/Express:
import type { Request, Response } from 'express';
export async function handleWebhook(req: Request, res: Response) {
const event = req.body; // Ensure raw body parsing if verifying signatures later
// De-duplicate using event.id
// if (await hasProcessed(event.id)) return res.sendStatus(200);
if (event.type != "Connection.Updated") {
return;
}
const data = event.data
switch (data.status) {
case 'data_available': {
const { connectionId, resources } = data
for (const resource of resources) {
if (resource.status == 'data_available'){
fetchResource(resource);
}
}
break;
}
default:
// ignore unknown types safely
break;
}
res.sendStatus(200);
}
Testing locally
- Expose your local server using a tunneling tool (e.g.,
ngrok http 3000). - Configure your webhook URL in Passage settings to the public tunnel URL.
- Log received events and store
idvalues to ensure idempotency.