Webhooks
Get a signed callback when an intent completes, and verify it came from Swivy.
Preview: Swivy is not generally available
Swivy runs in staging only. The event list will grow before general availability. Request early access.
In plain words: instead of polling an intent until it's done, give Swivy a URL. When the intent finishes, Swivy POSTs a small JSON message there, signed with a secret only you and Swivy know.
Register an endpoint
Server only, with an organization admin credential:
curl -X POST "$SWIVY_BASE_URL/v1/control/environments/env_.../webhooks" \
-H "content-type: application/json" \
-H "x-swivy-admin-key: $SWIVY_ADMIN_KEY" \
-d '{ "url": "https://example.com/swivy/webhooks" }'The response reveals the signing secret once. Store it like a password. You can list endpoints, rotate a secret (POST .../webhooks/{webhookId}/rotate-secret) and revoke an endpoint (DELETE .../webhooks/{webhookId}).
Endpoints must be public HTTPS-reachable hosts. Swivy refuses private or local addresses, URLs with credentials and redirects.
Events
Event type | When | Body |
|---|---|---|
swivy.intent.completed | A signing or transaction intent finished successfully. | { "type", "intentId", "environmentId", "result" } |
{
"type": "swivy.intent.completed",
"intentId": "int_...",
"environmentId": "env_...",
"result": { "transactionHash": "0x..." }
}Verify the signature
Each delivery carries an x-swivy-signature header: the hex HMAC-SHA256 of the raw JSON body, keyed with your endpoint's secret. Compute it over the exact bytes you received and compare in constant time.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
app.post('/swivy/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
const expected = crypto
.createHmac('sha256', process.env.SWIVY_WEBHOOK_SECRET)
.update(req.body) // raw bytes
.digest('hex');
const got = req.get('x-swivy-signature') ?? '';
const ok = got.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected));
if (!ok) return res.status(400).send('bad signature');
const event = JSON.parse(req.body);
if (event.type === 'swivy.intent.completed') {
// mark event.intentId done in your database (idempotently)
}
res.sendStatus(200);
});Delivery today
- Each completed intent is delivered once to every active endpoint, with a 10 second timeout.
- If a delivery fails, Swivy records it in the environment's audit log (
webhook.delivery_failed); there is no automatic retry yet. Keep pollingGET /v1/runtime/intents/{intentId}as a fallback for anything important. - The signature has no timestamp, so de-duplicate on
intentId.
Coming next (in review)
BuildingBuilt and in review, not on staging yet:
- Retries on the same schedule Privy uses: immediately, then 5s, 5m, 30m, 2h, 5h, 10h and 10h. After the last try the delivery is marked failed, and your server can list failed deliveries and replay them.
- More events:
swivy.identity.created,swivy.identity.authenticated,swivy.wallet.createdandswivy.intent.failed. - Every body gets an event
idandcreatedAt. De-duplicate onidand reject old events. Deliveries also carryx-swivy-event-idandx-swivy-delivery-attemptheaders. - The signature scheme stays the same.
Not planned yet: incoming-transfer (deposit) events and confirmation tracking.