Real-time events
Don't poll. Subscribe to the events you care about and Tenaro pushes them to your HTTPS endpoint the moment they happen, signed, retried, and replayable.
How it works
Register an endpoint
Webhook subscriptions are managed per workspace. Self-serve subscription UI in the dashboard is coming with Phase 1. Today, contact [email protected] and we'll set one up for you. You'll get a unique signing secret per endpoint.
Receive POSTs
Every event arrives as a JSON POST to your URL within seconds:
POST /your/webhook-endpoint HTTP/1.1
Content-Type: application/json
X-Tenaro-Event: payment.succeeded
X-Tenaro-Delivery: whd_9f2ac41be077d311
X-Tenaro-Signature: t=1735689600,v1=5d41402abc4b2a76b9719d911017c592…
{
"id": "evt_9f2ac41be077d311",
"type": "payment.succeeded",
"created": 1735689600,
"workspace": "yourworkspace",
"data": {
"payment_id": "pay_123",
"amount_cents": 185000,
"tenant_id": "tnt_456"
}
}
Return 2xx fast
Acknowledge within 10 seconds. Do heavy work asynchronously. We retry anything else.
Verifying signatures
Every delivery is HMAC-SHA256 signed with your endpoint's secret. Always verify. Never trust an unsigned POST.
import hmac, hashlib def verify(payload: bytes, header: str, secret: str) -> bool: # header: "t=1735689600,v1=5d4140…" parts = dict(p.split("=", 1) for p in header.split(",")) signed = parts["t"].encode() + b"." + payload expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, parts["v1"])
import crypto from "crypto"; function verify(payload, header, secret) { // header: "t=1735689600,v1=5d4140…" const parts = Object.fromEntries(header.split(",").map(p => p.split("="))); const signed = parts.t + "." + payload; const expected = crypto.createHmac("sha256", secret).update(signed).digest("hex"); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1)); }
function tenaro_verify_webhook(string $payload, string $header, string $secret): bool { // $header: "t=1735689600,v1=5d4140…" $parts = []; foreach (explode(",", $header) as $p) { [$k, $v] = explode("=", $p, 2) + [1 => ""]; $parts[$k] = $v; } if (!isset($parts["t"], $parts["v1"])) return false; $expected = hash_hmac("sha256", $parts["t"] . "." . $payload, $secret); return hash_equals($expected, $parts["v1"]); }
Check the timestamp
Reject deliveries where t is more than 5 minutes old, because it defeats replay attacks.
Idempotency
We may redeliver. Key your processing on the event id (evt_…) and dedupe.
Retries
Non-2xx or timeouts retry with exponential backoff over ~24 hours, then the delivery is marked failed and visible in your dashboard.
Events catalog
The v1 event set. Every event carries id, type, created (unix), workspace, and data.
Leases
| Event | Description |
|---|---|
lease.created | A lease was created. |
lease.renewed | A lease was renewed. |
lease.terminated | A lease was terminated. |
lease.expiring_soon | A lease enters its renewal window (configurable, default 60 days). |
Payments
| Event | Description |
|---|---|
payment.succeeded | A payment completed successfully. |
payment.failed | A payment attempt failed. Includes the failure reason. |
payment.refunded | A payment was refunded, fully or partially. |
invoice.overdue | An invoice passed its due date unpaid. |
charge.created | An ad-hoc charge (fee, utility bill-back) was created. |
Maintenance
| Event | Description |
|---|---|
maintenance.created | A maintenance request was submitted. |
maintenance.status_changed | A request moved between statuses. Includes old and new status. |
maintenance.completed | Work was marked complete. |
work_order.assigned | A vendor was assigned to a job. |
work_order.completed | A vendor marked their job complete. |
People & documents
| Event | Description |
|---|---|
tenant.invited | A tenant was invited to the portal. |
tenant.moved_in | A tenant's lease started. |
tenant.moved_out | A tenant's lease ended. |
document.uploaded | A document was uploaded. |
document.signed | A document was e-signed by all parties. |
user.deactivated | A user was deactivated. |
user.role_changed | A user's role changed. Includes old and new role. |
Get events flowing
Webhook subscriptions open with Phase 1. Tell us which events you need and we'll set up your endpoint.
Request webhook access