Skip to main content

Delivery

Every event is a POST to your webhook URL with a JSON body and these headers:
deliveryId identifies the delivery, not the event: a retried delivery keeps the same id. Use it to ignore duplicates.

Events

Every payload includes userRef, the identifier you passed to signLink(), so you can route the event without a lookup.

webhook.test

Sent once when your app is created and whenever you ask us to re-check your endpoint, with the same headers and signature as real events. Verify the signature if you like, answer 2xx, do nothing else. Unknown events in general should be acknowledged the same way: we may add event types.

connect.started

user_id is Verso’s internal id for the user. Nothing is connected yet; the user may still abandon the page.

connection.created

replaces lists previous connections of the same user and provider account, now revoked. Their conversations moved to connectionId. It is empty on a first connection.

data.imported

Not sent when a sync finds nothing new. inserted counts conversations seen for the first time, which during the initial backfill includes old history; use GET /api/connections for recent activity. historyComplete is false while older conversations are still being imported; the moment it turns true is also announced by history.completed.

history.completed

Sent once per connection, when the initial import reaches the end of the account’s conversation list: even when that last sync brought nothing new, and for accounts without any conversation (conversationCount 0, null dates). The figures are those of the connection at that moment: oldestConversationAt and newestConversationAt are the first message of the oldest and of the newest conversation, recentConversationCount the same activity metric as GET /api/connections. Anything that needs the whole history runs here: a rule on how far back it goes, a one-pass processing of the conversations, an “import finished” screen. The same figures are on GET /api/connections and GET /api/users (historyComplete, oldestConversationAt, newestConversationAt), so users connected before you handled this event need no replay. After a reconnect, the replacing connection goes through its own import and sends its own history.completed.

reauth.needed

Sign a new connect link for the user. Until they reconnect, the connection stays needs_reauth and its conversations remain readable.

data.deleted

connection.deleted

Verifying signatures

The signature is Stripe-style: t is the Unix timestamp of the delivery, v1 is HMAC-SHA256(secret, t + "." + body) in hex. Verify on the raw body.
verifyWebhook rejects signatures older than 5 minutes by default; pass a fourth argument to change the tolerance. Without the SDK:

Retries

Your endpoint must answer with a 2xx status within 10 seconds; Verso closes the connection after that. Anything else, including a timeout, counts as a failure and the delivery is retried with exponential backoff, six attempts in total. Apps are delivered in parallel, so another partner’s slow endpoint never delays yours. After the sixth failure the delivery is dropped. Deliveries are independent: a retried event can arrive after a newer one, so do not rely on ordering. If your app has no webhook URL configured, events are discarded.