DEVELOPERS

Webhooks

Tesska does not offer outbound webhooks yet. This page is upfront about the current status and the roadmap, and shows today's recommended alternative: polling the approval endpoint.

Status: outbound webhooks are not availablePlanned

We don't let the docs run ahead of the product: today, Tesska never calls back into your servers. If you find Tesska webhook settings or signature docs elsewhere, they are not ours. Webhooks are on the roadmap and will be announced in the docs and the console when they ship.

What we're planning

The first callbacks will be designed around two event families:

  • Approval results: get notified as soon as a Mode B (approval-required) request is decided, instead of polling GET /v1/approvals/:id.
  • Anomaly alerts: risk events delivered straight to your systems — today these alerts reach the credential owner's phone via push notification.

Today's alternative: poll the approval result

A Mode B call to POST /v1/login doesn't block: it returns pending_approval with a requestId right away and pushes the credential owner's phone. With the requestId in hand, polling GET /v1/approvals/:id covers the core webhook use case.

# Mode B login — returns immediately with a pending approval curl https://tesska.com/v1/login \ -X POST \ -H "Authorization: Bearer tsk_..." \ -H "Content-Type: application/json" \ -d '{ "platform": "github", "account": "deploy-bot", "clientPubKey": "<base64 ECDH-P256 public key>" }' # → { "kind": "pending_approval", "requestId": "...", "expiresAt": "..." }

Polling example

An official SDK hasn't shipped yet — the example below uses plain fetch, and any HTTP client works the same way:

// Poll GET /v1/approvals/:id until the owner approves. // Any HTTP client works — no SDK required. async function waitForApproval(requestId, expiresAt) { let delay = 2000; // start at 2s while (Date.now() < new Date(expiresAt).getTime()) { const res = await fetch( `https://tesska.com/v1/approvals/${requestId}`, { headers: { Authorization: "Bearer tsk_..." } }, ); if (res.status === 429) { // per-minute rate limit — back off harder delay = Math.min(delay * 2, 15000); } else { const body = await res.json(); if (body.status === "approved") { // body.resultCt is E2E ciphertext — decrypt locally with // the ECDH private key you kept from the /v1/login call return body; } // not decided yet — gentle exponential backoff delay = Math.min(delay * 1.5, 15000); } await new Promise((r) => setTimeout(r, delay)); } throw new Error("Approval request expired without a decision"); }

Polling gives up no security.The approved resultCt is end-to-end ciphertext — the credential is decrypted in the owner's browser and sealed to the ECDH-P256 public key you sent with /v1/login. Only the matching private key can open it; the server never sees plaintext, no matter how the result is delivered.

Suggested intervals and backoff

  • Start at 2 seconds: a human approval typically lands within seconds to minutes — polling faster buys you nothing.
  • Exponential backoff: multiply the interval by 1.5–2 on every undecided poll, capped at 15 seconds.
  • Respect the rate limit: project keys are rate-limited per minute; on a 429, double the backoff instead of retrying immediately.
  • Treat expiresAt as the deadline: once the approval request expires, stop polling and give the user clear feedback.
  • Polling requests count toward the audit log and the rate limit, just like any other /v1/* call.