Events
Reference for every webhook event type Solwyn emits, with per-type payload shape and an example payload.
Solwyn webhooks deliver budget and circuit-breaker event types today, with two
provider-health types reserved in the contract for future emitters. Use a
webhook's event filter (* or a comma-separated list of event types) to
control which events your endpoint receives.
Currently emitted
These event types are delivered today.
budget_threshold
Fired when a project's spend crosses one of its configured budget thresholds below 100% (for example, 50% or 80% of the period budget). Each threshold is fired at most once per period — once the threshold has been crossed for the period, it will not refire even if usage dips and recrosses.
Payload data fields:
| Field | Type | Description |
|---|---|---|
project_name | string | Human-readable project name. |
budget_limit | number | Period budget in USD. |
current_usage | number | Period spend in USD at time of fire. |
threshold_pct | integer | The threshold that was crossed (e.g. 50, 80). |
period | string | One of daily, weekly, monthly. |
period_date | string | Period bucket: YYYY-MM-DD for daily, YYYY-Www (ISO week) for weekly, YYYY-MM for monthly. |
channels | array | Notification channels configured for this threshold (e.g. ["webhook", "email"]). |
Example:
{
"event_type": "budget_threshold",
"project_id": "proj_a1b2c3d4e5f6a7b8c9d0e1f2",
"timestamp": "2026-05-05T18:00:00.123456+00:00",
"idempotency_key": "notification:7f3a9d2c815e4b6f0a1d8c3e5b7f9a2d4c6e8b0f1a3d5c7e9b2f4a6c8e0d1b3f",
"data": {
"project_name": "production",
"budget_limit": 100.0,
"current_usage": 80.5,
"threshold_pct": 80,
"period": "monthly",
"period_date": "2026-05",
"channels": ["webhook", "email"]
}
}budget_exceeded
Fired when a project's spend crosses 100% of its period budget. Same payload
shape as budget_threshold, with threshold_pct set to the configured 100%
threshold. This is a separate event type so you can route it to a more urgent
channel than the lower-percentage warnings.
Example:
{
"event_type": "budget_exceeded",
"project_id": "proj_a1b2c3d4e5f6a7b8c9d0e1f2",
"timestamp": "2026-05-05T19:30:00.654321+00:00",
"idempotency_key": "notification:2b8e6d4f0c9a7315e8b2d6f4a0c8e1b3d7f5a9c2e6b0d8f4a2c6e0b4d8f2a6c1",
"data": {
"project_name": "production",
"budget_limit": 100.0,
"current_usage": 102.4,
"threshold_pct": 100,
"period": "monthly",
"period_date": "2026-05",
"channels": ["webhook", "email", "pagerduty"]
}
}circuit_breaker_opened
Fired when an SDK instance reports its per-provider circuit breaker transitioning into the open state — including the first snapshot Solwyn Cloud receives from an instance when that snapshot already reports an open breaker. Severity is critical. Transitions into half-open are silent, as are replayed or unchanged snapshots.
The producer is the SDK's
breaker state reporting
(SDK v0.3.0+, on by default via breaker_reporting_enabled). Events are
scoped to a single project, provider, and SDK instance: every instance reports
its own in-process breaker, so several instances calling the same provider each
fire their own events — one instance's open breaker does not mean the provider
is down for everyone. Breaker events deliver over webhooks and the notification
channels; they do not appear in the dashboard's in-app alerts feed, which stays
budget-only.
Payload data fields:
| Field | Type | Description |
|---|---|---|
provider | string | Provider the breaker tracks (e.g. openai). |
provider_name | string | Same value as provider — both are sent. |
state | string | Reported breaker state; open for this event type. |
failure_count | integer | Consecutive failures recorded against the breaker. |
success_count | integer | Consecutive successes recorded against the breaker. |
reason | string | Why the breaker opened. Opened events only. |
reported_at | string | ISO 8601 UTC time the SDK took the snapshot — distinct from the envelope's dispatch timestamp. |
sdk_instance_id | string | The reporting client instance's id (a fresh UUID per SDK client instance). |
Example:
{
"event_type": "circuit_breaker_opened",
"project_id": "proj_a1b2c3d4e5f6a7b8c9d0e1f2",
"timestamp": "2026-05-05T20:14:03.221417+00:00",
"idempotency_key": "breaker-transition:e3d1c5a7f9b2d4e6a8c0f1b3d5e7a9c2f4b6d8e0a1c3f5b7d9e2a4c6f8b0d1e3",
"data": {
"provider": "openai",
"provider_name": "openai",
"state": "open",
"failure_count": 5,
"success_count": 0,
"reason": "Circuit breaker reported open after 5 failures",
"reported_at": "2026-05-05T20:14:01.008932+00:00",
"sdk_instance_id": "8f14e45f-ceea-467a-9f2c-3b1d6a7e40c5"
}
}circuit_breaker_closed
Fired when a reported breaker returns to closed from either open or half-open —
the recovery counterpart to circuit_breaker_opened. Severity is info. A
first-ever snapshot that arrives already closed is silent, as are replayed or
unchanged snapshots. Scope and producer are the same as the opened event: one
project, one provider, one SDK instance.
Each project's delivery defaults carry a severity threshold (warning by
default). This event is info-severity but is deliberately exempt from the
severity floor — resolution events always pass the severity filter, so an
incident that opened can always be closed out. The per-target event filter still
applies: turn the event type off and you receive nothing.
Payload data fields: the same shape as circuit_breaker_opened, without
reason, plus:
| Field | Type | Description |
|---|---|---|
downtime_minutes | integer | Whole minutes the breaker was open. Closed events only. |
Example:
{
"event_type": "circuit_breaker_closed",
"project_id": "proj_a1b2c3d4e5f6a7b8c9d0e1f2",
"timestamp": "2026-05-05T20:31:47.905118+00:00",
"idempotency_key": "breaker-transition:4a7c1e9b3d5f2a8c6e0b4d7f1a3c9e5b8d2f6a0c4e7b1d9f3a5c8e2b6d0f4a7c",
"data": {
"provider": "openai",
"provider_name": "openai",
"state": "closed",
"failure_count": 0,
"success_count": 2,
"downtime_minutes": 17,
"reported_at": "2026-05-05T20:31:45.412006+00:00",
"sdk_instance_id": "8f14e45f-ceea-467a-9f2c-3b1d6a7e40c5"
}
}Reserved
provider_outage and provider_recovered are part of the contract — adapters
are wired up to receive them and webhook filters accept them as valid values —
but nothing emits them yet. Circuit-breaker events carry the per-instance view
of provider health, and a provider-wide outage is a different claim: one SDK
instance's breaker opening does not establish that a provider is down for your
account. These two types stay reserved until an emitter can make that
account-wide determination.
If you build handlers for these now, expect to revise the payload shape once the emitters ship; the field names below reflect the consumer-side expectation, not a frozen contract.
provider_outage
Will fire when an upstream LLM provider (OpenAI, Anthropic, Google) is detected as experiencing an outage that affects projects on your account.
Expected data fields: provider_name, detected_at, affected_projects.
provider_recovered
Will fire when a previously degraded provider returns to healthy operation.
Expected data fields: provider_name, downtime_minutes.
Idempotency
Every delivery carries an Idempotency-Key HTTP request header and a matching
idempotency_key field in the JSON payload. The key identifies the event
itself and is stable across retries and redeliveries, so dedupe on it
directly — a "have I seen this key before" check is all you need. For
circuit-breaker transitions the key is a stable hash of the project, provider,
SDK instance, state, and snapshot time, so the same transition always resolves
to the same key.
The signature covers the full payload including idempotency_key, so
verification against the raw bytes needs no change — see
Verifying signatures.
The natural key for each event type is still useful for correlating events with your own records:
- Budget events:
(event_type, project_id, period, period_date, threshold_pct)uniquely identifies a single threshold crossing for a project's budget period. The same threshold will not refire within the same period bucket. - Circuit-breaker events:
(event_type, project_id, provider, sdk_instance_id, reported_at)identifies one instance's breaker transition. Correlate an opened event with its closed event onproject_id,provider, andsdk_instance_id.
Solwyn does not retry events past the 3-attempt delivery limit, so duplicate deliveries are limited to those 3 attempts. A simple "have I seen this key in the last hour" check is sufficient.