Essentials

Webhooks

Octo notifies your backend of events by POSTing signed JSON to URLs you register. Today the main event is deposit.created, fired when a deposit confirms on-chain and is attributed to one of your addresses.

Register an endpoint

POST/v1/wallets/:id/webhooks
Request
curl -X POST http://localhost:8080/v1/wallets/<WALLET_ID>/webhooks \
  -H "authorization: Bearer octo_sk_test_ab12…" \
  -H "content-type: application/json" \
  -d '{ "url": "https://your.app/webhooks/octo" }'

The response includes a signing secret, shown once. You use it to verify deliveries.

The URL must be a public http(s) endpoint. Loopback and private addresses are rejected.

Event payload

POST to your URL
X-Octo-Signature: <hmac-sha256 hex>
Content-Type: application/json

{
  "event": "deposit.created",
  "data": {
    "id": "1f2e…",
    "wallet_id": "52775…",
    "address_id": "8d22…",
    "asset_code": "native",
    "amount_stroops": 50000000,
    "source_account": "GA…",
    "stellar_tx_hash": "7f18…",
    "memo_id": 7,
    "status": "confirmed",
    "attributed": true,
    "metadata": { "plan": "pro" }
  }
}

The metadata is exactly what you attached when creating the address — use it to reconcile the deposit to your user.

Verify the signature

Each delivery includes an X-Octo-Signature header: the lowercase hex HMAC-SHA256 of the raw request body using your signing secret. Recompute it and compare in constant time before trusting the event.

Node.js
import crypto from "node:crypto";

function verify(rawBody, signature, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)            // the exact bytes received
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature),
  );
}

app.post("/webhooks/octo", (req, res) => {
  const sig = req.header("X-Octo-Signature");
  if (!verify(req.rawBody, sig, process.env.OCTO_WEBHOOK_SECRET)) {
    return res.status(401).end();
  }
  const { event, data } = JSON.parse(req.rawBody);
  // credit data.metadata's user by data.amount_stroops …
  res.status(200).end();
});
Verify against the raw body bytes, not a re-serialized object — re-serializing can change the bytes and break the signature.

Delivery & retries

  • Respond with a 2xx to acknowledge.
  • Non-2xx responses are retried with exponential backoff; every attempt is logged.
  • Deposits are deduplicated on-chain, so design your handler to be idempotent.