tutorial

How to Monitor Stripity Stripe with Vigilmon

Monitor Stripe payment processing health, webhook delivery, subscription job reliability, and API latency in your Elixir app using Stripity Stripe and Vigilmon uptime monitoring.

How to Monitor Stripity Stripe with Vigilmon

Stripity Stripe is the most widely used Elixir client for the Stripe API. It handles charges, subscriptions, customers, webhooks, refunds, and the full Stripe API surface — complete with retry logic and Telemetry instrumentation. When payments are involved, silent failures cost real money: a webhook that stops delivering, a subscription renewal that times out, or a Stripe API key rotation that breaks your integration.

External monitoring catches what your application and Stripe's own dashboard can't correlate in real time. This tutorial covers:

  • A health endpoint that validates your Stripe API connectivity
  • HTTP uptime monitoring with Vigilmon
  • Heartbeat monitoring for subscription renewal and webhook workers
  • Telemetry-based latency tracking
  • Slack alerts and a status page

Step 1: Add Stripity Stripe to your project

# mix.exs
defp deps do
  [
    {:stripity_stripe, "~> 3.0"},
    {:req, "~> 0.4"}   # for heartbeat pings
  ]
end

Configure your Stripe API key:

# config/config.exs
config :stripity_stripe,
  api_key: System.get_env("STRIPE_SECRET_KEY"),
  httpoison_options: [timeout: 10_000, recv_timeout: 10_000]

For retry behaviour (built-in to Stripity Stripe):

config :stripity_stripe,
  api_key: System.get_env("STRIPE_SECRET_KEY"),
  retry_count: 3

Step 2: Build a health endpoint for Stripe connectivity

# lib/my_app_web/plugs/health_check.ex
defmodule MyAppWeb.Plugs.HealthCheck do
  import Plug.Conn
  require Logger

  def init(opts), do: opts

  def call(%Plug.Conn{request_path: "/health"} = conn, _opts) do
    checks = run_checks()
    status = if checks.status == "ok", do: 200, else: 503

    conn
    |> put_resp_content_type("application/json")
    |> send_resp(status, Jason.encode!(checks))
    |> halt()
  end

  def call(conn, _opts), do: conn

  defp run_checks do
    stripe = stripe_check()
    overall = stripe.status == "ok"

    %{
      status: if(overall, do: "ok", else: "degraded"),
      stripe: stripe
    }
  end

  defp stripe_check do
    start = System.monotonic_time(:millisecond)

    # Retrieve the Stripe account — lightweight connectivity probe
    result =
      case Stripe.Account.retrieve() do
        {:ok, account} ->
          elapsed = System.monotonic_time(:millisecond) - start
          {:ok, elapsed, account.id}

        {:error, %Stripe.Error{} = err} ->
          {:error, err.message}
      end

    case result do
      {:ok, latency_ms, account_id} ->
        %{status: "ok", account_id: account_id, latency_ms: latency_ms}

      {:error, reason} ->
        Logger.error("[Stripe] health check failed: #{reason}")
        %{status: "error", reason: reason}
    end
  end
end

Register the plug before the router:

# lib/my_app_web/endpoint.ex
plug MyAppWeb.Plugs.HealthCheck
plug MyAppWeb.Router

Test it:

mix phx.server
curl http://localhost:4000/health | jq .
# {
#   "status": "ok",
#   "stripe": {
#     "status": "ok",
#     "account_id": "acct_1abc...",
#     "latency_ms": 312
#   }
# }

Step 3: Attach Telemetry for Stripe API latency

Stripity Stripe emits Telemetry events on every API call. Capture them to expose per-endpoint latency in your health response:

# lib/my_app/telemetry.ex
defmodule MyApp.Telemetry do
  require Logger

  def setup do
    :telemetry.attach(
      "stripe-request-stop",
      [:stripity_stripe, :request, :stop],
      &handle_stripe_event/4,
      nil
    )
  end

  def handle_stripe_event([:stripity_stripe, :request, :stop], measurements, meta, _config) do
    duration_ms = System.convert_time_unit(measurements.duration, :native, :millisecond)

    if duration_ms > 5_000 do
      Logger.warning("[Stripe] slow request to #{meta.endpoint}: #{duration_ms}ms")
    end

    :telemetry.execute([:my_app, :stripe, :latency], %{ms: duration_ms}, %{endpoint: meta.endpoint})
  end
end

Call MyApp.Telemetry.setup/0 from your application start:

# lib/my_app/application.ex
def start(_type, _args) do
  MyApp.Telemetry.setup()
  # ...
end

Step 4: Monitor the health endpoint with Vigilmon

  1. Sign up at vigilmon.online
  2. Click New Monitor → HTTP
  3. Enter https://yourdomain.com/health
  4. Set check interval: 1 minute
  5. Enable keyword check: body must contain "status":"ok"
  6. Save

The keyword check catches degraded states: if Stripe's API is returning errors your endpoint returns HTTP 200 with "status":"degraded", and Vigilmon alerts you before customers see failed checkouts.


Step 5: Heartbeat monitoring for subscription workers

Subscription renewals and webhook deliveries are the lifeblood of a SaaS payment flow. Heartbeat monitoring confirms they ran on schedule:

# lib/my_app/workers/subscription_renewal_worker.ex
defmodule MyApp.Workers.SubscriptionRenewalWorker do
  use GenServer
  require Logger

  @interval :timer.minutes(60)

  def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)

  def init(state) do
    schedule()
    {:ok, state}
  end

  def handle_info(:renew, state) do
    process_due_renewals()
    schedule()
    {:noreply, state}
  end

  defp process_due_renewals do
    # Retrieve and process subscriptions due for renewal
    case Stripe.Subscription.list(%{status: "active"}) do
      {:ok, %{data: subscriptions}} ->
        Logger.info("[SubscriptionWorker] processed #{length(subscriptions)} subscriptions")
        ping_vigilmon()

      {:error, reason} ->
        Logger.error("[SubscriptionWorker] failed: #{inspect(reason)}")
    end
  end

  defp ping_vigilmon do
    url = Application.get_env(:my_app, :vigilmon)[:heartbeat_url]

    if url do
      case Req.get(url, receive_timeout: 5_000) do
        {:ok, %{status: 200}} -> :ok
        err -> Logger.warning("[SubscriptionWorker] heartbeat ping failed: #{inspect(err)}")
      end
    end
  end

  defp schedule, do: Process.send_after(self(), :renew, @interval)
end

Add it to your supervision tree:

# lib/my_app/application.ex
children = [
  MyApp.Repo,
  MyAppWeb.Endpoint,
  MyApp.Workers.SubscriptionRenewalWorker
]

In Vigilmon, create a Heartbeat Monitor:

  1. Click New Monitor → Heartbeat
  2. Set expected interval: 90 minutes
  3. Copy the ping URL
  4. Set it as VIGILMON_HEARTBEAT_URL in your environment

If the worker crashes or the Stripe API is unresponsive, the heartbeat stops — and Vigilmon alerts you before the next billing cycle is affected.


Step 6: Monitor Stripe webhook ingestion

Webhook delivery failures are silent by default. Add a separate monitor for your webhook endpoint:

# lib/my_app_web/controllers/stripe_webhook_controller.ex
defmodule MyAppWeb.StripeWebhookController do
  use MyAppWeb, :controller
  require Logger

  def handle(conn, _params) do
    payload = conn.assigns[:raw_body]
    sig = get_req_header(conn, "stripe-signature") |> List.first()
    secret = Application.get_env(:my_app, :stripe_webhook_secret)

    case Stripe.Webhook.construct_event(payload, sig, secret) do
      {:ok, event} ->
        process_event(event)
        send_resp(conn, 200, "ok")

      {:error, reason} ->
        Logger.error("[StripeWebhook] invalid signature: #{inspect(reason)}")
        send_resp(conn, 400, "bad request")
    end
  end

  defp process_event(%{type: "invoice.payment_succeeded"} = event) do
    Logger.info("[StripeWebhook] payment succeeded for #{event.data.object.customer}")
  end

  defp process_event(%{type: "customer.subscription.deleted"} = event) do
    Logger.info("[StripeWebhook] subscription cancelled: #{event.data.object.id}")
  end

  defp process_event(event) do
    Logger.debug("[StripeWebhook] unhandled event type: #{event.type}")
  end
end

Add a second HTTP monitor in Vigilmon pointing to https://yourdomain.com/webhooks/stripe with a POST method check to confirm the endpoint is accepting requests.


Step 7: Slack alerts and status page

Slack alerts:

  1. In Vigilmon, go to Notifications → New Channel → Slack
  2. Paste your Slack incoming webhook URL
  3. Enable the channel on your Stripe health monitor and subscription heartbeat

When Stripe connectivity drops:

🔴 DOWN: yourdomain.com/health
Keyword "status":"ok" not found
Detected: EU-West
1 minute ago

When the subscription worker misses a cycle:

🔴 HEARTBEAT MISSED: Subscription Renewal Worker
Expected ping within 90 minutes — none received
Last seen: 97 minutes ago

Status page:

  1. Go to Status Pages → New Status Page
  2. Add your HTTP monitor and heartbeat monitor
  3. Share with your team — or make it public for customers during incidents

What you've built

| What | How | |------|-----| | Stripe connectivity check | Account retrieve probe with latency | | Uptime monitoring | Vigilmon HTTP monitor → /health | | Keyword check | Vigilmon keyword match on "status":"ok" | | Telemetry latency | Stripity Stripe Telemetry events + slow request logging | | Subscription worker monitoring | Heartbeat GenServer + Vigilmon heartbeat monitor | | Webhook endpoint monitoring | Vigilmon HTTP POST monitor | | Slack alerts | Vigilmon Slack notification channel | | Status page | Vigilmon public status page |

Stripity Stripe connects your app to Stripe's payment infrastructure. Vigilmon tells you when that connection — or the workers built on top of it — silently stops working.


Next steps

  • Alert on Stripe API latency trending above 1 second using Vigilmon's response time history, which can signal key rotation issues or rate-limit pressure
  • Monitor your Stripe webhook secret rotation by surfacing webhook signature failures in your health response
  • Use Vigilmon's multi-region checks to confirm your payment endpoints are reachable from all regions where customers make purchases

Get started free at vigilmon.online.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →