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:
- Enter your address:
https://only, on a public address. - Save, then copy your signing secret: it is shown only once. Saving the address again gives a new secret.
- Send a test event: a
pingarrives within a minute. The last deliveries and their status are listed below.
Events
| Event | When |
|---|---|
workspace.ready | The client's workspace is ready: its invitation link is in inviteLink |
payment.failed | A payment for the workspace failed (the card, 3D Secure...) |
workspace.suspended | The workspace was suspended for non-payment |
workspace.deleted | The workspace was deleted (subscription ended) |
ping | A 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.readyv1 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
idto do the work only once.