Geta.Team Geta.Team
API

Webhooks

Hear of your workspaces' events on your own backend, signed.

Give Geta.Team an https address and your backend hears of each event of your clients' workspaces, within a minute after it happens.

Set it up

In your dashboard, Account, section API, Webhook:

  1. Enter your address: https:// only, on a public address.
  2. Save, then copy your signing secret: it is shown only once. Saving the address again gives a new secret.
  3. Send a test event: a ping arrives within a minute. The last deliveries and their status are listed below.

Events

EventWhen
workspace.readyThe client's workspace is ready: its invitation link is in inviteLink
payment.failedA payment for the workspace failed (the card, 3D Secure...)
workspace.suspendedThe workspace was suspended for non-payment
workspace.deletedThe workspace was deleted (subscription ended)
pingA test event, from the dashboard

Each post is JSON:

{
  "id": "evt_8434fff1b5dc162847b6ac3f",
  "type": "workspace.ready",
  "created": "2026-10-29T10:06:03.000Z",
  "data": {
    "workspace": {
      "id": 77, "clientId": 501, "clientName": "Acme", "clientEmail": "[email protected]",
      "status": "running", "url": "https://acme-x.geta.team",
      "inviteLink": "https://acme-x.geta.team/invite/...", "employees": 1
    }
  }
}

Extra fields: paymentUrl and graceHours for payment.failed; paymentUrl for workspace.suspended; reason for workspace.deleted.

Check the signature

Every post carries:

Geta-Signature: t=1793872803,v1=5f0c...e91
Geta-Event-Id: evt_8434fff1b5dc162847b6ac3f
Geta-Event-Type: workspace.ready

v1 is the HMAC-SHA256, in hex, of t, a dot, then the raw body, keyed with your secret (the whole whsec_... string). Check it on the raw body, before any JSON parsing, and refuse a t more than 5 minutes old (a replayed request).

// Node.js (Express): app.post("/geta/webhooks", express.raw({ type: "application/json" }), handler)
const crypto = require("crypto");

function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const given = Buffer.from(parts.v1 || "", "hex");
  return given.length === 32 && crypto.timingSafeEqual(given, Buffer.from(expected, "hex"));
}
# Python
import hmac, hashlib, time

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t = int(parts.get("t", "0"))
    if abs(time.time() - t) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))
// PHP
function verify(string $rawBody, string $header, string $secret): bool {
  parse_str(str_replace(',', '&', $header), $parts);
  $t = (int)($parts['t'] ?? 0);
  if (abs(time() - $t) > 300) return false;
  $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
  return hash_equals($expected, $parts['v1'] ?? '');
}

Answer, retries

  • Answer any 2xx status quickly (within 10 seconds), then do the work.
  • Anything else, or no answer, is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours, then given up.
  • The same event may come twice: use its id to do the work only once.

On this page