Skip to content

Webhooks ​

Receive real-time notifications when teams are provisioned, deleted, when monitors change, when incidents occur in managed teams, and when platform emails are suppressed.

Setup ​

Create a webhook subscription in a single call:

bash
curl -X POST https://app.hoppla.dev/api/v1/reseller/webhooks \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Ops Integration",
    "url": "https://your-server.com/hoppla-webhook",
    "secret": "your-hmac-secret",
    "events": [
      "monitor.down",
      "monitor.up",
      "incident.created",
      "incident.resolved",
      "ssl.expiring",
      "team.provisioned"
    ],
    "tenant_filter": null
  }'

This creates both a notification channel and rule automatically. Manage with GET/PATCH/DELETE /reseller/webhooks/{id}. View delivery logs with GET /reseller/webhooks/{id}/deliveries.

Option B: Manual Setup (Advanced) ​

For full control over channel settings (quiet hours, rate limits, batch windows), use the standard channel + rule endpoints:

1. Create a Notification Channel ​

bash
curl -X POST https://app.hoppla.dev/api/v1/notification-channels \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Provisioning Webhooks",
    "type": "webhook",
    "config": {
      "url": "https://your-server.com/hoppla-webhook",
      "secret": "your-hmac-secret"
    },
    "is_enabled": true
  }'

2. Create a Notification Rule ​

bash
curl -X POST https://app.hoppla.dev/api/v1/notification-rules \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "channel_id": "channel-uuid-...",
    "event_types": [
      "team.provisioned",
      "team.deleted",
      "incident.created",
      "incident.resolved",
      "monitor.down",
      "ssl.expiring"
    ],
    "tenant_filter": {
      "tenant_ids": ["uuid1", "uuid2"],
      "labels": ["production-*"]
    }
  }'

Tenant Filtering ​

Filter which managed tenants trigger events for each webhook:

FilterDescription
null / omittedAll managed tenants (default)
tenant_idsExplicit list of tenant UUIDs
labelsPattern match on managed team labels (supports trailing * wildcard)

Filters are OR-combined: an event matches if it matches any tenant_id OR any label pattern.

Complete Event Reference ​

Team Management Events ​

EventTriggerKey Payload Fields
team.provisionedTeam created via API or portal registrationmanaged_team_id, tenant_id, tenant_name, label
team.deletedTeam soft-deletedmanaged_team_id, tenant_id, tenant_name
team.pausedMonitoring paused for a teammanaged_team_id, tenant_id, reason
team.unpausedMonitoring resumedmanaged_team_id, tenant_id
team.trial_endingTrial expires in 3 days or 1 daymanaged_team_id, tenant_id, plan, trial_ends_at, days_remaining
team.trial_expiredTrial period ended, plan reverted to freemanaged_team_id, tenant_id, previous_plan
tenant.quota_warningA managed team reaches ≥80% of a plan limit (e.g. monitors)tenant_id, feature, current_count, limit, usage_pct
tenant.quota_exceededA create was blocked because a managed team hit a plan limittenant_id, feature, current_count, limit, usage_pct

Quota events are deduplicated per team + feature (one tenant.quota_warning per day, one tenant.quota_exceeded per 6 hours) so a batch import does not flood your webhook.

Monitor Events ​

EventTrigger
monitor.createdMonitor created in a managed team
monitor.updatedMonitor updated in a managed team
monitor.deletedMonitor deleted in a managed team
monitor.downMonitor went down (convenience alias for incident.created)
monitor.upMonitor recovered (convenience alias for incident.resolved)
monitor.enabledMonitor enabled (was paused/disabled)
monitor.disabledMonitor disabled/paused
monitor.flappingMonitor is flapping (rapidly toggling up/down)
monitor.flapping_resolvedMonitor stopped flapping and stabilized
ssl.expiringSSL certificate expiring within threshold (alias for warning.ssl_expiring)

Member Events ​

EventTrigger
member.addedUser added to a managed team
member.removedUser removed from a managed team
member.erasedA member's personal data was erased under GDPR Art. 17

member.erased is not member.removed. Removal frees a seat: the person still exists and can be re-invited. Erasure destroys their identity across the whole platform — login, passkeys, device tokens, and every team they belonged to, including teams you do not manage. If your CRM treats member.removed as "mark the seat free, keep the contact", that is the wrong handling here: the contact record is what has to go.

It fires whether the person erased themselves from their own account settings or you erased them on their behalf via POST /reseller/managed-teams/{managedTeam}/members/{userId}/erase. The payload's initiated_by is self or reseller.

This event carries the e-mail address deliberately — it is the only key you can match against your own records, and it is the last message that can carry it.

Monitoring Events (Forwarded) ​

All monitoring events from your managed teams are automatically forwarded to your notification channels. These are the same events your managed teams' users receive — you always have full visibility.

CategoryEvent Types
Incidentsincident.created, incident.resolved
Warningswarning.ssl_expiring, warning.domain_expiring, warning.degraded_performance, warning.application_health, warning.sitemap_issues, warning.security_headers, warning.redirect_chain, warning.robots_txt, warning.dnsbl_listing, warning.broken_links, warning.mixed_content, warning.server_resources, warning.cleared
DNSdns.records_changed, dns.nameserver_changed
Certificatescertificate.renewed
Stormsstorm.detected, storm.resolved

dns.nameserver_changed is a convenience alias fired in addition to dns.records_changed whenever a confirmed DNS change includes a change to the authoritative nameserver set (an NS-hijack signal). It carries the same payload as dns.records_changed. Like all forwarded DNS events, it is delivered to the reseller independently of the managed customer's notify_dns_changes toggle — a customer silencing their own DNS alerts never blinds the reseller to a nameserver change on their fleet.

Email Suppression Events ​

When you suppress platform emails for your managed teams, Hoppla fires email.* webhooks containing the full email payload — so you can send the emails yourself.

EventSuppressed Email
email.password_resetNever fired — password-reset mail cannot be suppressed.
email.email_change_verifyNever fired — address-change verification cannot be suppressed.
email.email_changed_notifyNever fired — the notice to the old address cannot be suppressed.
email.invitationSuppressed team invitation mail.
email.member_removedSuppressed notice that a member was removed.
email.trial_endingSuppressed trial-ending reminder.
email.trial_expiredSuppressed trial-expired notice.
email.incident_createdSuppressed incident-opened alert.
email.incident_resolvedSuppressed incident-resolved all-clear.
email.warningSuppressed advisory-warning mail.
email.dns_changedSuppressed DNS-change notice.
email.certificate_renewedSuppressed certificate-renewal notice.
email.digestSuppressed periodic digest.
email.monthly_reportSuppressed monthly monitoring report.
email.status_page_subscriptionSuppressed status-page update to a subscriber.
email.security_mfa_enabledSuppressed notice that two-factor authentication was switched on.
email.security_mfa_disabledSuppressed notice that two-factor authentication was switched off.
email.security_recovery_codes_regeneratedSuppressed notice that recovery codes were replaced, invalidating the old set.
email.security_mfa_resetSuppressed notice that two-factor authentication was reset by an administrator.
email.security_passkey_addedSuppressed notice that a passkey was registered to the account.
email.security_passkey_removedSuppressed notice that a passkey was removed from the account.
email.security_password_changedSuppressed notice that the account password was changed.
email.security_email_changedSuppressed notice that the account email address was changed.
email.security_api_token_createdSuppressed notice that an API token was issued.
email.security_api_token_revokedSuppressed notice that an API token was revoked.
email.security_session_revokedSuppressed notice that a signed-in session was ended remotely.
email.security_login_new_deviceSuppressed notice of a sign-in from a device not seen before.
email.security_account_blockedSuppressed notice that the account was blocked from signing in.
email.security_account_unblockedSuppressed notice that a block on the account was lifted.
email.security_role_changedSuppressed notice that the account's role in a team changed.
email.security_auth_policy_changedSuppressed notice that the team's sign-in policy changed.
email.security_account_deletedSuppressed notice that the account was deleted.

Suppression requires a listener

An email is only suppressed if you have an enabled webhook channel with a rule subscribed to that exact email.* event. Without one the platform email is sent to the user as normal, rather than being dropped with nothing in its place. Subscribe first, then enable suppression.

The auth class is never suppressed

email.password_reset, email.email_change_verify and email.email_changed_notify are listed for completeness and never fire. Their template data is the credential itself — a working reset or verification link — so delivering it to a reseller would be an account takeover the user is never told about. Use impersonation if you need to act on a managed account; that route is audited as impersonation.

Payload Examples ​

Team Provisioned ​

json
{
  "event": "team.provisioned",
  "timestamp": "2026-01-15T10:30:00.000000Z",
  "managed_team_id": "mt-uuid-...",
  "tenant_id": "new-tenant-uuid-...",
  "tenant_name": "Acme Corp",
  "label": "Enterprise Client #42"
}

Team Trial Ending ​

json
{
  "event": "team.trial_ending",
  "timestamp": "2026-01-28T00:00:00.000000Z",
  "managed_team_id": "mt-uuid-...",
  "tenant_id": "tenant-uuid-...",
  "tenant_name": "Acme Corp",
  "label": "Enterprise Client #42",
  "plan": "pro",
  "trial_ends_at": "2026-01-31T00:00:00.000000Z",
  "days_remaining": 3
}

Member Erased ​

json
{
  "event": "member.erased",
  "email": "[email protected]",
  "name": "A Person",
  "erased_at": "2026-09-22T08:14:02+00:00",
  "initiated_by": "self",
  "erasure_id": "01a0c7...",
  "teams": [
    {
      "managed_tenant_id": "01a0c7...",
      "user_id": "01a0c7...",
      "role": "admin",
      "label": "acme-prod",
      "external_ref": "CRM-4711"
    }
  ]
}

One event per reseller, not per team: if the person belonged to several of your teams, every one is listed under teams. A managed team is never deleted by an erasure, even when the erased person was its last member — it is left in place with no members so the decision to close or re-staff it stays yours.

Forwarded Incident ​

Forwarded monitoring events include a _managed_tenant object so you can identify which team triggered the event:

json
{
  "event": "incident.created",
  "timestamp": "2026-01-15T14:00:00.000000Z",
  "incident_id": "inc-uuid-...",
  "monitor_id": "monitor-uuid-...",
  "monitor_name": "Acme Website",
  "monitor_url": "https://acmecorp.com",
  "title": "HTTP 503 Service Unavailable",
  "status": "investigating",
  "severity": "major",
  "started_at": "2026-01-15T14:00:00.000000Z",
  "resolved_at": null,
  "_managed_tenant": {
    "managed_tenant_id": "tenant-uuid-...",
    "name": "Acme Corp",
    "label": "Enterprise Client #42"
  }
}

Forwarded DNS Change ​

Both dns.records_changed and dns.nameserver_changed carry the same zone-aware payload. nameserver_change is null (and nameservers_changed is false) for a routine record change; when the authoritative nameserver set changes it describes the before/after delegation:

json
{
  "event": "dns.nameserver_changed",
  "timestamp": "2026-01-15T16:00:00.000000Z",
  "monitor_id": "monitor-uuid-...",
  "monitor_name": "Acme Website",
  "monitor_url": "https://acmecorp.com",
  "check_type": "dns",
  "zone": "acmecorp.com",
  "nameservers": ["ns1.evil-host.com", "ns2.evil-host.com"],
  "changes": [
    { "type": "NS", "old": ["ns1.acme-dns.com", "ns2.acme-dns.com"], "new": ["ns1.evil-host.com", "ns2.evil-host.com"] }
  ],
  "nameserver_change": {
    "old": ["ns1.acme-dns.com", "ns2.acme-dns.com"],
    "new": ["ns1.evil-host.com", "ns2.evil-host.com"]
  },
  "nameservers_changed": true,
  "_managed_tenant": {
    "managed_tenant_id": "tenant-uuid-...",
    "name": "Acme Corp",
    "label": "Enterprise Client #42"
  }
}

Email Suppression Webhook ​

When a platform email is suppressed, the webhook contains everything you need to send the email yourself:

json
{
  "event": "email.security_password_changed",
  "timestamp": "2026-01-15T15:00:00.000000Z",
  "email_type": "security_password_changed",
  "recipient": {
    "email": "[email protected]",
    "name": "John Doe",
    "language": "en",
    "user_id": "user-uuid-..."
  },
  "template_data": {
    "user_name": "John Doe",
    "user_email": "[email protected]",
    "brand_name": "Acme Monitoring",
    "occurred_at": "2026-01-15T14:59:58+00:00",
    "ip_address": "203.0.113.10"
  },
  "_managed_tenant": {
    "managed_tenant_id": "tenant-uuid-...",
    "name": "Acme Corp",
    "label": "Enterprise Client #42"
  }
}

This example used to show email.password_reset carrying a working reset_url. That event does not fire and that payload is never sent — see the note on the auth class above.

Key Behaviors ​

  • Same event type names — resellers subscribe to incident.created, not reseller.incident.created. The _managed_tenant field distinguishes forwarded events.
  • Monitor mute bypass — if a managed team mutes notifications for a monitor, you still receive the forwarded event. Resellers always have full visibility.
  • Cached lookups — managed team lookups are cached for 5 minutes and automatically invalidated when relationships change.

Email Suppression ​

Resellers can suppress specific platform emails for all managed teams. When an email is suppressed, Hoppla fires an email.* webhook carrying the recipient and the template data instead of sending the mail, so you can send it yourself under your own branding.

Suppression is a redirect, not an off switch. It only takes effect when you are subscribed to the matching event, and the auth class is never suppressed at all — see the two notes above.

Configure Suppression ​

bash
# View current config
curl https://app.hoppla.dev/api/v1/reseller/email-suppression \
  -H "Authorization: Bearer $TOKEN"

# Update suppressed email types
curl -X PUT https://app.hoppla.dev/api/v1/reseller/email-suppression \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "suppressed_types": [
      "invitation",
      "member_removed",
      "incident_created",
      "incident_resolved",
      "warning"
    ]
  }'

How It Works ​

  1. You configure which email types to suppress via the API or dashboard
  2. When Hoppla would normally send one of these emails to a managed team user, it checks your suppression config
  3. If the email type is suppressed, Hoppla fires an email.* webhook instead
  4. Your system receives the webhook with the full email payload and can send the email using your own templates and branding
  5. Non-email notification channels (Slack, Telegram, etc.) are not affected — they continue to fire normally

Important Notes ​

  • Suppression applies to all your managed teams (reseller-wide setting)
  • Only the email channel is suppressed — other notification channels still work normally
  • The payload carries the template data for the mail you are replacing. It never carries password-reset or address-verification links, because those emails cannot be suppressed
  • A subscription is required. Suppressing a type you are not subscribed to has no effect: the platform sends the email as normal. This is deliberate — the alternative is a message that reaches nobody
  • If your endpoint is down the email is still suppressed (there is no fallback to platform email), so treat delivery failures on email.* as user-visible

Delivery ​

  • Events are dispatched asynchronously in the background
  • Failed deliveries are retried 3 times with backoff (10s, 60s, 300s)
  • HTTP delivery timeout: 10 seconds — your endpoint must respond within 10s or the delivery is treated as failed and retried
  • Delivery status is logged and viewable via the notification logs API

Supported Channels ​

Reseller events can be delivered through any notification channel type:

ChannelDescription
WebhookHTTP POST with JSON payload and optional HMAC signature
EmailEvent details sent to configured recipients
SlackMessage posted via incoming webhook
DiscordMessage posted via Discord webhook
Microsoft TeamsMessage posted via Teams incoming webhook
PagerDutyEvent routed via PagerDuty Events API
TelegramMessage sent via Telegram Bot API

Verifying Webhook Signatures ​

If you configured a secret on your webhook channel, each request includes an X-Webhook-Signature header with an HMAC-SHA256 signature. (The former X-Hoppla-Signature header was removed on 28 September 2026; it carried the same value.)

Verify against the raw request body — not a re-serialized object. The signature is computed over the exact bytes Hoppla sends. Hoppla builds that body by sorting the top-level keys alphabetically and serializing JSON without escaping forward slashes. If you parse the JSON and re-encode it before verifying, the bytes will differ (key order, slash escaping, whitespace) and verification will fail. Always read the raw request body and pass those bytes directly to your HMAC function.

python
import hmac
import hashlib

# payload_body MUST be the raw request bytes, before any JSON parsing
def verify_signature(payload_body: bytes, secret: str, signature_header: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        payload_body,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header)
php
// Read the raw body, e.g. file_get_contents('php://input') — do not re-encode a decoded array
$expected = hash_hmac('sha256', $rawRequestBody, $secret);
$valid = hash_equals($expected, $signatureHeader);