docs
Get startedTroubleshootingPaymentsWebhooksAgent paymentsSwop MCPOviIdentity (swop.id)SwivyRPC (preview)API reference

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 typeWhenBody
swivy.intent.completedA 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 polling GET /v1/runtime/intents/{intentId} as a fallback for anything important.
  • The signature has no timestamp, so de-duplicate on intentId.

Coming next (in review)

Building

Built 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.created and swivy.intent.failed.
  • Every body gets an event id and createdAt. De-duplicate on id and reject old events. Deliveries also carry x-swivy-event-id and x-swivy-delivery-attempt headers.
  • The signature scheme stays the same.

Not planned yet: incoming-transfer (deposit) events and confirmation tracking.

On this page