Skip to content
Metric VaultHelp Center
Open app

Webhooks

The inbound Stripe webhook the platform receives, and the outbound Slack and JSON webhooks that Competitor Monitor and rank alerts send.

Last updated 2026-08-06

Summary#

Metric Vault receives one inbound webhook, from Stripe, which is what keeps plan entitlements in step with subscriptions. It sends two kinds of outbound webhook, both belonging to the alerting features: a formatted Slack message and a generic JSON payload you can point at any HTTPS endpoint. There are no webhooks for tool completion, and the public API is synchronous, so nothing calls you back after an /api/v1/analyze request.

Overview#

DirectionEndpoint or destinationTriggerConfigured where
InboundPOST /api/stripe/webhookSubscription lifecycle eventsThe Stripe dashboard, plus a signing secret in the environment
OutboundYour Slack incoming-webhook URLA monitored page changes, or a keyword rank dropsPer monitored URL, or per rank alert
OutboundAny HTTPS endpoint you ownSame triggersPer monitored URL, or per rank alert
OutboundAn email addressSame triggersPer monitored URL, or per rank alert

Inbound: the Stripe webhook#

Endpoint#

PropertyValue
PathPOST /api/stripe/webhook
AuthenticationHMAC-SHA256 signature in the stripe-signature header
BodyThe raw Stripe event, read before any other processing so the bytes stay intact
PurposeGrants and revokes plan entitlements as subscriptions change

Signature verification#

The signature is computed over the string <timestamp>.<raw body> using the webhook signing secret, then compared in constant time against the v1= values in the stripe-signature header. Events older than 300 seconds are rejected as replays.

Important

Important: If the signing secret is not configured, the endpoint fails closed: every call is answered 503 with code: "webhook_not_configured" and the message Webhook is not configured (STRIPE_WEBHOOK_SECRET is not set). Entitlements then drift until the reconciliation job catches them. This is one of the first things to check when a customer pays and does not get their plan.

Handled events#

Event typeAction
checkout.session.completedGrants the purchased plan to the customer's email
customer.subscription.createdGrants the plan implied by the subscription
customer.subscription.updatedGrants or revokes according to the new status
customer.subscription.deletedRevokes the plan
Anything elseAcknowledged and ignored

A subscription is treated as entitling when its status is active, trialing or past_due. Keeping past_due deliberately preserves access during dunning rather than cutting a paying customer off on the first failed charge. See Subscription states and Failed payments.

Responses#

HTTPBodyCause
200{"received":true,"type":"<event>","action":"granted"}Plan granted
200{"received":true,"type":"<event>","action":"revoked"}Plan revoked
200{"received":true,"type":"<event>","action":"ignored"}An event type that is not handled
400{"error":"Invalid signature"}Signature mismatch or a stale timestamp
400{"error":"unreadable body"}The raw body could not be read
400{"error":"invalid JSON"}The body was not valid JSON
500{"error":"DB unavailable"}The database was unreachable. Returned deliberately so Stripe retries
503{"error":"Webhook is not configured (STRIPE_WEBHOOK_SECRET is not set).","code":"webhook_not_configured"}No signing secret

Safety property#

Subscription events write the plan only when the existing record was itself set by Stripe. A manually granted or comped plan is never overwritten by an incoming webhook, so a support-issued upgrade survives billing traffic.

The reconciliation backstop#

Webhooks can be missed. POST /api/billing/sync pages through live Stripe subscriptions and reconciles stored plans against them. It also runs as the billing_sync background job, so entitlements self-heal within a scheduling cycle even if a webhook never arrives. It grants freely but only revokes when it has completed a full scan, and it only ever revokes records that Stripe set. See Internal endpoints.


Outbound: alert webhooks#

Two features send outbound webhooks: Competitor Monitor and rank alerts. Both use the same dispatcher, so the payloads are identical in shape.

Where they are configured#

FeatureFieldSet on
Competitor Monitorslack_webhook_url, generic_webhook_url, notify_email_addrEach monitored URL, at POST /api/monitor/add or POST /api/monitor/update
Rank alertsThe same three fieldsEach alert, at POST /api/rank-alerts/create

All three channels are optional and independent. Supplying none means the change is recorded in the app and appears in the notifications feed, but nothing is pushed anywhere.

Note

Note: Webhook URLs are validated when you save them. A URL pointing at a loopback, private, link-local or otherwise reserved address is rejected with 400 Webhook host is not allowed. Use a public HTTPS endpoint.

The severity gate#

Each monitored URL carries a min_severity of Low, Medium or High. Changes below that level are recorded but never dispatched. Rank alerts always dispatch at Low, so every rank drop that passes the drop threshold is sent.

Slack payload#

Sent as POST with Content-Type: application/json to your Slack incoming webhook URL. It uses Slack's Block Kit format: a header block reading Watchtower alert, a section naming the monitored label and linking the URL, a section listing up to 8 changes as • *Severity* — description with an …and N more line when there are more, and a context line showing the severity gate, the check interval, and a link back to Competitor Monitor.

The fallback text field reads *Watchtower* — change detected on <label>.

Generic JSON payload#

Sent as POST with Content-Type: application/json to your endpoint. This is the one to point at Zapier, Make, or your own service.

json
{
  "source": "Metric Vault Watchtower",
  "event": "changes_detected",
  "monitored": {
    "id": 128,
    "url": "https://competitor.example.com/pricing",
    "label": "Competitor pricing page",
    "competitor_domain": "competitor.example.com"
  },
  "changes": [
    {
      "severity": "High",
      "change_type": "price_drop",
      "description": "Pro plan price fell from $99 to $79",
      "old_value": "99",
      "new_value": "79",
      "numeric_delta": -20
    }
  ],
  "top_change": {
    "severity": "High",
    "description": "Pro plan price fell from $99 to $79"
  },
  "change_count": 1,
  "detected_at": "2026-08-06T09:15:00.000Z",
  "dashboard_url": "https://metricvaultai.com/dashboard#competitor-monitor"
}
FieldTypeNotes
sourcestringAlways Metric Vault Watchtower
eventstringAlways changes_detected
monitored.idnumberThe monitored URL's identifier
monitored.urlstringFor a rank alert this is the Google results URL for the keyword
monitored.labelstringYour label, or for a rank alert Rank drop: "<keyword>"
monitored.competitor_domainstringThe domain, or for a rank alert the domain being tracked
changesarrayOnly changes at or above the severity gate
changes[].change_typestringFor rank alerts this is rank_drop
changes[].numeric_deltanumber or nullThe numeric movement where one exists
top_changeobjectThe first change, for a one-line summary
change_countnumberLength of changes
detected_atstringISO 8601 timestamp
dashboard_urlstringDeep link back into the app

Rank-alert specifics#

A rank alert fires when the position drops by at least the alert's threshold (1 to 20, default 5) or when a keyword that was in the top 10 leaves it. The change carries:

FieldValue
change_typerank_drop
severityHigh when the keyword left page one, or when the drop is at least twice the threshold. Otherwise Medium
description"<keyword>" dropped from #4 to #11 on Google for example.com, or "<keyword>" no longer ranks on page 1 for example.com (was #4)
old_valueThe previous position
new_valueThe new position, or not ranked
numeric_deltaPositions lost, or null when the keyword left the results

Enabled rank alerts are re-checked roughly every 24 hours.

Email channel#

If an address is set and email sending is configured, an HTML message is sent from Watchtower <alerts@metricvaultai.com> with the subject Watchtower: <N> change(s) on <label>, listing up to 12 changes.

Delivery semantics#

These are the properties that matter when you build a receiver:

PropertyBehavior
RetriesNone. One attempt per channel per detection
SigningNone. Outbound payloads are not signed. Treat the webhook URL itself as the secret
OrderingNot guaranteed
Delivery logNot exposed. Failures are logged server-side and swallowed
Failure impactA failing webhook never blocks or fails the underlying check
TimeoutsThe dispatcher does not wait indefinitely; a slow endpoint is simply counted as not delivered
DuplicatesPossible if a check runs twice over the same change window
Tip

Tip: Because there are no retries and no signatures, build your receiver to be idempotent on monitored.id plus detected_at, keep the webhook URL secret and unguessable, and treat a missed delivery as normal rather than exceptional. The app's own notifications feed is the authoritative record.

Testing a webhook#

POST /api/monitor/test-alert dispatches a synthetic price_drop change at High severity to whichever channels you supply in the body, with the description Test alert — your alert channels are wired correctly. It does not require an existing monitored URL and does not cost credits.


Other callback-shaped mechanisms#

These are not webhooks in the usual sense, but they are HTTP callbacks and it helps to know they exist.

MechanismWhat it is
OAuth callbacks/auth/google/callback and /api/social/callback/<platform> complete an authorisation flow. State is stored server-side, is single use, and expires after 15 minutes
Scheduler triggerPOST /api/cron/run is called by an external scheduler to run due background jobs

What does not exist#

Being explicit saves an integration built on a wrong assumption:

  • No webhooks for tool completion. Running a tool does not call you back. The dashboard tools are request-response, including long article jobs.
  • No webhooks for the public API. POST /api/v1/analyze returns its result in the response.
  • No subscription-management API for webhooks. Outbound endpoints are configured per monitored URL or per rank alert, not through a webhook registry.
  • No signing secret or signature header on outbound payloads.
  • No delivery history, retry queue or dead-letter view.
  • No webhooks for billing, usage or team events.

See also

Was this article helpful?