Webhooks
Webhooks let you receive HTTP POST requests whenever events happen in your workspaces. Instead of polling for changes, you get notified the moment a VM stops, a workload fails, or your credit balance drops.
How it works
- You register a webhook URL and choose which events to subscribe to
- When a matching event fires, Oblien sends a JSON POST to your URL
- Your endpoint processes the payload and returns a 2xx status
- Oblien records the delivery status for debugging
Supported events
Billing events
payment.succeeded, subscription.renewed, subscription.tier_changed,
subscription.updated, subscription.past_due, subscription.canceled,
entitlement.changed, namespace.suspended, and namespace.restored support the
customer billing lifecycle. Register billing hooks from your backend using an
admin credential. Reseller payment events carry the saved offer, application
metadata, namespace allowance and wallet funding separately. See
Billing and Build a SaaS or VM reseller.
Compute capacity, transfer and retained storage
| Events | Customer state |
|---|---|
capacity.changed, capacity.renewed | A paid or funded compute contract changed or renewed |
capacity.expired, capacity.payment_required, capacity.revoked | Compute coverage ended, requires funding or was revoked |
network.topup_applied, network.allowance.low, network.allowance.depleted | Managed proxy transfer was purchased, is low or has run out |
network.allowance.grace | Courtesy transfer grace began; internet remains available within its bytes/deadline |
network.allowance.blocked, network.allowance.restored | Managed internet transfer is blocked after funding/grace, or has recovered; paid compute is not canceled |
network.policy.updated | An owner changed spending settings, or a provider policy revision was applied to the paid period (source: "provider"); re-read capacity.network |
storage.retention.payment_required, storage.retention.paid | Retained storage has an amount due or was settled |
These use the existing durable, signed billing delivery flow and require owner billing permissions. Re-read billing.capacity(namespace) and billing.balance(namespace) after an event. Show compute coverage, transfer and retained-storage balances separately. A paid monthly server has no compute-credit exhaustion warning; a transfer alert alone does not revoke its compute coverage. See PAYG and monthly compute.
Workspace events
| Event | Description |
|---|---|
vm.stopped | Workspace VM has shut down |
vm.archived | VM removed on exit (ephemeral / temporary mode) |
Workload events
| Event | Description |
|---|---|
workload.started | A process started inside the VM |
workload.exited | A process finished cleanly |
workload.failed | A process exited with an error |
workload.stopped | A process was force-stopped |
workload.restart_loop | A process is stuck in a crash loop |
Credit & quota events
These describe existing credit allowances and credit-billed services. They do not replace the capacity lifecycle events above.
| Event | Description |
|---|---|
credits.usage | Fired on each billing cycle — includes namespace, workspace, and detailed resource breakdown |
credits.low | A capped namespace entered configured credit grace |
credits.depleted | A namespace exhausted its allowance and grace; account-only workspaces retain account-wallet signals |
namespace.quota.threshold | A namespace quota crossed one of its configured notification thresholds (e.g. 80%, 95%). Idempotent per cycle — fires once per threshold per quota reset |
Credit events include rich context so you can track per-namespace and per-workspace spend:
{
"event": "credits.usage",
"timestamp": "2026-03-12T14:30:00.000Z",
"data": {
"balance": 142.5,
"credits_used": 3.25,
"namespace": "production",
"workspace_id": "ws_abc123",
"workspace_name": "my-agent",
"period_start": "2026-03-12T14:25:00.000Z",
"period_end": "2026-03-12T14:30:00.000Z",
"usage": {
"cpu_time_minutes": 4.2,
"memory_gb_minutes": 2.1,
"disk_io_gb": 0.003,
"network_gb": 0.0015
}
}
}The namespace.quota.threshold event lets you notify your own customers as their workspaces approach their spending caps. Configure the percentages per quota via notificationThresholds (defaults to [80, 95], pass [] to disable). Thresholds use the included allowance plus purchased top-ups. The debit, marker and alert commit together, and delivery retries after a failure or restart. A cycle reset or funding that lowers usage below a band re-arms it. Read quota.alert on billing entitlement before notifying, because delayed events may describe an older balance.
{
"event": "namespace.quota.threshold",
"timestamp": "2026-03-12T14:30:00.000Z",
"data": {
"namespace": "acme-corp",
"service": "workspace_vm",
"threshold": 80,
"percent": 81.42,
"used": 814.2,
"limit": 1000,
"workspace_id": "ws_abc123",
"workspace_name": "my-agent"
}
}Mode A: your backend resets each new paid cycle. Mode B: verified paid subscription renewals reset usage and re-arm warnings automatically. Custom notification bands survive renewals. Deduplicate signed event IDs before queueing email or in-app notifications.
Payload format
Every webhook delivery is a POST request with a JSON body:
{
"event": "vm.stopped",
"timestamp": "2026-03-12T14:30:00.000Z",
"data": {
"workspace_id": "ws_abc123",
"reason": "ttl_expired"
}
}The payload shape under data varies by event type.
Signature verification
If you provide a secret when creating the webhook, every delivery includes an X-Webhook-Signature header containing an HMAC-SHA256 hex digest of the raw request body.
To verify:
import crypto from 'crypto';
function verifyWebhook(body, signature, secret) {
if (typeof signature !== 'string' || !/^[a-f0-9]{64}$/i.test(signature)) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(body)
.digest();
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
expected
);
}Always use a timing-safe comparison to prevent timing attacks.
Pass the original raw request bytes, not reserialized JSON. Always configure a
secret for billing hooks. Verify the signature before parsing or acting; the
JSON id must match X-Webhook-Id. Atomically deduplicate that ID and persist
processing work before returning success. Re-fetch billing state to handle
out-of-order events.
Delivery behavior
- Timeout: 10 seconds per delivery attempt
- Fire-and-forget: Failed deliveries are not retried automatically
- Status tracking: The last HTTP status code and trigger time are recorded per webhook
- Limit: Up to 10 webhooks per account
Reconcile pending payments and current entitlement through the Billing API when a delivery is missed. Stripe's incoming payment webhooks are a separate flow; Oblien's outgoing notifications do not have Stripe's automatic retry behavior.
Scope a webhook to a namespace
A webhook can be bound to a single namespace so it only receives events
attributable to that namespace — its credits.usage, namespace.quota.threshold,
and the vm/workload events for workspaces in it. Omit namespace to make the
webhook account-wide (every matching event, the default).
This is ideal for multi-tenant setups: give each tenant a webhook scoped to its namespace and it sees only its own usage.
import Oblien from 'oblien';
const client = new Oblien({ clientId, clientSecret });
// All usage events for one namespace only:
await client.webhooks.create({
url: 'https://example.com/hooks/oblien',
events: ['credits.usage', 'namespace.quota.threshold'],
namespace: 'team-a', // ← scope; omit for account-wide
secret: process.env.WH_SECRET,
});
const { webhooks } = await client.webhooks.list({ namespace: 'team-a' });A namespace-scoped token is pinned to its own namespace for operational hooks.
It cannot manage billing hooks or modify/delete a sibling or account-wide hook.
Keep billing hooks under your backend's admin credentials; a billing key alone
does not grant webhook-management access.
Managing webhooks
Create, update, toggle, and delete webhooks from the Dashboard, the SDK, or the API. Two equivalent surfaces:
| Surface | Base | Auth |
|---|---|---|
| SDK / API client | /webhooks | API client credentials (X-Client-ID / X-Client-Secret) or a scoped token |
| Dashboard | /developer/webhooks | Logged-in session (JWT) |
| Method | Endpoint | Description |
|---|---|---|
GET | /webhooks[?namespace=] | List webhooks (namespace-filterable) |
GET | /webhooks/events | List subscribable event types |
POST | /webhooks | Create a webhook (url, events, optional secret, description, namespace) |
PUT | /webhooks/:id | Update a webhook |
DELETE | /webhooks/:id | Delete a webhook |
Best practices
- Always verify signatures if you set a secret — don't trust payloads blindly
- Respond quickly — return a 200 within a few seconds, do heavy processing async
- Use HTTPS endpoints — never send webhook traffic over plain HTTP
- Handle duplicates — deduplicate the stable JSON
id/X-Webhook-Id, not a timestamp or workspace ID - Monitor delivery status — check the dashboard for failed status codes