Documentation Index
Fetch the complete documentation index at: https://paper.brimble.io/llms.txt
Use this file to discover all available pages before exploring further.
To configure delivery, see Webhooks.
Envelope
Every webhook delivery is a POST with Content-Type: application/json. The body has this shape:
interface WebhookPayload<T> {
event: WebhookEvent;
data: T; // shape depends on the event, see below
}
There is no subscription_id, no timestamp, no project field at the top level. Resource information lives inside data.
Common types
These are referenced from multiple events.
type ID = string;
type ISODate = string;
type ServiceType = "web-service" | "static" | "worker" | "database" | "mcp";
type ProjectStatus =
| "PENDING"
| "INPROGRESS"
| "ACTIVE"
| "FAILED"
| "CANCELLED"
| "INACTIVE"
| "DEGRADED"
| "PAYMENT";
type Environment = "PRODUCTION" | "PREVIEW";
type GitType = "github" | "gitlab" | "bitbucket" | "docker";
interface Repo {
name: string;
full_name: string;
id: number;
branch: string;
installationId: number;
git: GitType;
}
interface Compute {
cpu: number; // vCPU (fractional)
memory: number; // GB
storage: number; // GB
}
interface Region {
id: ID;
name: string;
continent: string;
}
Deployment events
Sent for deployment.success and deployment.failed.
{
event: "deployment.success" | "deployment.failed";
data: {
name: string; // branch name
status: ProjectStatus;
user_id: ID;
project_id: ID;
team_id: ID | null;
type: ServiceType;
git: Repo | null;
compute: Compute;
message: string; // commit message
};
}
Project events
Sent for project.created, project.updated, and project.deleted.
{
event: "project.created" | "project.updated" | "project.deleted";
data: {
name: string;
user_id: ID;
project_id: ID;
team_id: ID | null;
type: ServiceType;
region: Region;
git: Repo | null;
compute: Compute;
created_at: ISODate;
};
}
Project domain updated
Sent for project.domain.updated.
{
event: "project.domain.updated";
data: {
name: string;
user_id: ID;
project_id: ID;
team_id: ID | null;
domain: { new: string };
domain_id: ID;
updated_at: ISODate;
};
}
Domain events
Sent for domain.created, domain.purchased, and domain.renewed.
{
event: "domain.created" | "domain.purchased" | "domain.renewed";
data: {
domain: string; // the domain name
domain_id: ID;
user_id: ID;
project_id: ID;
team_id: ID | null;
registrar: "Brimble Inc";
purchased: boolean;
price: number; // renewal price
expires_at: ISODate;
purchased_at?: ISODate; // present on domain.purchased
created_at?: ISODate; // present on domain.created
};
}
DNS record events
Sent for dns.record.created, dns.record.updated, and dns.record.deleted.
{
event: "dns.record.created" | "dns.record.updated" | "dns.record.deleted";
data: {
user_id: ID;
domain: string; // parent domain name
domain_id: ID;
team_id: ID | null;
record: {
id: ID;
name: string; // host portion
type: string; // "A" | "AAAA" | "CNAME" | "MX" | "NS" | "TXT" | "SRV" | "CAA"
value: string;
ttl: number; // seconds
isProxied?: boolean;
};
created_at?: ISODate; // present on dns.record.created
updated_at?: ISODate; // present on dns.record.updated
};
}
Environment variable events
Sent for environment.variables.added, environment.variables.updated, and environment.variables.deleted.
{
event: "environment.variables.added" | "environment.variables.updated" | "environment.variables.deleted";
data: {
user_id: ID;
project_id: ID;
team_id: ID | null;
environment: Environment | string;
variable: string; // the variable name only — value is never sent
created_at?: ISODate;
updated_at?: ISODate;
};
}
The variable’s value is not included in the payload. Only the variable name. Webhook handlers can react to “this variable changed” without seeing the secret.
Autoscaling events
Sent for autoscaling.group.created, autoscaling.group.updated, and autoscaling.group.deleted.
{
event: "autoscaling.group.created" | "autoscaling.group.updated" | "autoscaling.group.deleted";
data: {
name: string;
autoscaling_group_id: ID;
user_id: ID;
team_id: ID | null;
status: boolean; // active flag
min_instances: number;
max_instances: number;
desired_instances: number;
scaling_policy: {
cpu_threshold: number;
memory_threshold: number;
};
created_at?: ISODate;
updated_at?: ISODate;
};
}
Subscription events
Sent for subscription.created, subscription.upgraded, subscription.downgraded, subscription.renewed, subscription.renewal_failed, subscription.past_due, subscription.unpaid, subscription.paused, subscription.reactivated, subscription.canceled, and subscription.incomplete_expired.
{
event: `subscription.${string}`;
data: {
subscription: {
id: ID;
stripe_id: string;
stripe_status: string; // "active" | "past_due" | "unpaid" | ...
plan_type: string; // "free-plan" | "hacker-plan" | ...
type: string; // subscription "name", usually "default"
ends_at?: ISODate; // on subscription.canceled
previous_plan?: string; // on subscription.upgraded
current_plan?: string; // on subscription.upgraded
previous_price?: string; // on subscription.downgraded
};
user: {
id: ID;
email: string;
};
team: { id: ID } | null; // null for personal subscriptions
cancellation_reason?: string | null;
failure_reason?: string | null;
invoice?: {
stripe_invoice_id: string;
amount_paid?: number;
amount_due?: number;
currency?: string;
attempt_count?: number;
};
};
}
Payment events
Sent for payment.successful and payment.failed.
{
event: "payment.successful" | "payment.failed";
data: {
invoice: {
stripe_invoice_id: string;
amount_paid?: number; // payment.successful
amount_due?: number; // payment.failed
currency: string; // "usd", "ngn", etc.
status: string; // "paid" | "open" | ...
paid?: boolean; // payment.successful
attempt_count?: number; // payment.failed
};
user: { id: ID; email: string };
subscription: {
id: ID;
stripe_id: string;
plan_type: string;
stripe_status: string;
};
team: { id: ID } | null;
failure_reason?: string | null; // payment.failed
};
}
Subscribing
Pass the events you want when configuring a webhook, or ["*"] to receive everything. Pass [] to disable delivery without removing the configuration.
Delivery model
- At-least-once. A delivery may be retried after a failed response. Make handlers idempotent. Keying on
data.project_id (or data.domain_id, etc.) plus event plus the relevant timestamp works for most resources.
- Order is not guaranteed. Two events from the same project can arrive out of order. Use
created_at / updated_at from data to reconcile.
- Best-effort. After repeated delivery failures, a single delivery is dropped. Your webhook itself stays enabled and continues to receive subsequent events.