Releaseo

Product

Monitor

Monitor a business-critical process like checkout or signup. Your backend reports each step with one HTTP call; Releaseo shows a live funnel and alerts Slack or Discord on failure or completion.

Releaseo Monitor observes the health of a business-critical process — checkout, signup, password reset, a data import — the steps your own backend controls. Declare the monitor’s steps once in the dashboard, then call one HTTP endpoint from your backend as each step happens. Releaseo turns those calls into a live step funnel, a searchable run timeline, and a Slack or Discord alert when a step fails or a run completes.

Monitor is not an APM or request-tracing tool, and it is not session or user tracking. There is no SDK involved and nothing runs in a visitor’s browser — Releaseo only knows what your backend explicitly reports for the steps you declared, nothing more.

Quickstart

1. Create a monitor from a template

Open Dashboard → Monitor and start from a template — Signup, Checkout & payment, Onboarding provisioning, Password reset, Subscription change, or Data import — or start blank and define your own steps. Each step gets a key (for example payment_authorized) and a label. Mark one step terminal; a run counts as completed when that step reports success.

2. Generate a server key

Monitor events are authenticated with a dedicated secret key, separate from the browser-safe SDK key used elsewhere in Releaseo. Open Settings → Environment → Server Runtime and generate one. It looks like sk_live_… and is shown in full so you can copy it into your backend’s own configuration.

3. Send your first event

Call the endpoint from your backend as the step happens. Here is the first step of the Checkout & payment template:

curl -X POST https://api.releaseo.io/api/public/flows/events \
  -H "x-project-key: $RELEASEO_SERVER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "flow": "0197a1c0-5f6d-7a20-b7c5-2f6f6f6f6f01",
    "ref": "req_8f3a",
    "step": "started",
    "status": "success",
    "metadata": {
      "source": "checkout-api",
      "referrerHost": "app.example.com",
      "referrerPath": "/checkout",
      "user.id": "user_42"
    }
  }'

ref is your correlation label — a request id, order id, or job id works well. Send related steps with the same label. Reusing it combines their recorded events; it does not establish a unique execution or retry attempt. Use the optional execution format below when you need explicit attempt outcomes.

4. Watch it fill in

Open the monitor’s Overview tab. The step funnel fills in as events arrive, the run appears in the recent-runs list, and — if you send a status: "failure" event — it shows up in the failure feed too. Events start appearing as soon as you send them; wiring up Slack/Discord alerts is a separate opt-in step, covered below.

Execution attempts and final outcomes

Execution recording is an optional server feature. Enable it only after the schema-v2 rollout is complete. Its separate endpoint returns 503 while disabled, and older servers return 404. Clients must not fall back to the legacy endpoint, because that would discard execution identity.

POST /public/flows/execution-events
x-project-key: <your server key>

Send one object or a batch of up to 100. Keep the existing event fields and add an execution envelope. This example reports an explicitly failed attempt:

{
  "flow": "0197a1c0-5f6d-7a20-b7c5-2f6f6f6f6f01",
  "ref": "order-123",
  "step": "payment_authorized",
  "status": "failure",
  "occurredAt": "2026-10-18T12:00:00.000Z",
  "error": "Payment provider declined the request",
  "metadata": { "region": "eu-west-1" },
  "execution": {
    "id": "0197a1c0-5f6d-7a20-b7c5-2f6f6f6f6f02",
    "eventId": "0197a1c0-5f6d-7a20-b7c5-2f6f6f6f6f03",
    "attempt": 1,
    "sequence": 2,
    "outcome": "failure",
    "error": {
      "code": "PAYMENT_DECLINED",
      "type": "PaymentError",
      "retryable": false,
      "httpStatus": 402
    }
  }
}
  • Generate a distinct execution UUID for each operation. Keep it for retries of that operation and increase the explicit attempt number, starting at 1.
  • Give each event a stable UUID and each event in an attempt a distinct sequence, starting at 1. Retransmit the identical event object and occurrence time after a delivery failure. Do not generate another event id for that retry.
  • occurredAt is required in canonical UTC milliseconds, for example 2026-10-18T12:00:00.000Z. It must be within seven days of the server’s current time, including retransmissions. An old event cannot be replayed beyond that horizon by changing its timestamp; that changes the recorded event payload.
  • Omit outcome for intermediate steps. Report success or failure only when the attempt finishes; it must match the event’s status. A failed step by itself does not declare the attempt finished.
  • Structured errors require status: "failure". The optional stackTrace is bounded to 4,096 characters. Remove credentials and unnecessary personal data before recording error context.
  • 202 means queued acceptance. MCP execution tools deduplicate event identities within the selected window; legacy dashboard totals and event notifications retain their existing behavior. This is not exactly-once alert delivery.

MCP exposes attempt comparisons, execution pages and individual timelines after execution recording is enabled. Detail reads require recorded-detail permission; raw error fields, stack traces and selected metadata additionally require explicit field selection. Conflicting identities, sequences or final reports appear as ambiguous. A missing final report is unreported. Missing steps remain explicit, and legacy references are never converted into invented executions.

API reference

Endpoint and auth

POST /public/flows/events

Every request carries the server key from step 2 in the x-project-key header. There is no project id anywhere in the URL — the key itself resolves the project.

Response Meaning
401 Unauthorized The x-project-key header is missing, or the key value is unknown.
403 Forbidden The key exists but has been disabled.
403 { code: "server_key_required" } The key is valid and enabled, but is not a server key — “This endpoint requires a server-scope project key, not a browser SDK key.”
202 { "accepted": <count> } The batch passed validation and was queued.

Payload

Send one event object, or a JSON array of up to 100 for a batch:

{
  "flow": "0197a1c0-5f6d-7a20-b7c5-2f6f6f6f6f01",
  "ref": "req_8f3a",
  "step": "payment_authorized",
  "status": "success",
  "error": "Optional error text",
  "message": "Optional free-text note, up to 512 characters",
  "metadata": { "source": "checkout-worker", "region": "eu-west-1" },
  "env": "production",
  "occurredAt": "2026-01-01T00:00:00.000Z"
}
Field Type Required Notes
flow UUID string Yes ID of an enabled monitor in this project. Copy it from your monitor; the UUIDs in these examples are placeholders.
ref string Yes Up to 128 characters. Identifies one run; reusing a ref merges into the same run instead of starting a new one.
step string Yes Key of a declared step on that monitor.
status "success" | "failure" | "pending" Yes pending means the step was reached but has not reported terminal success or failure yet. It is recorded as reached, but does not count as a completion or failure and emits no Monitor alert.
error string No Up to 2000 characters. Shown verbatim in the run timeline and failure feed.
message string No Up to 512 characters. A short free-text note, shown in the run timeline and — on a failed step — forwarded to Slack/Discord alerts.
metadata flat object of strings No Up to 24 entries; keys up to 64 characters (letters, digits, ., _, -); values up to 512 characters. String values only — no nested objects, arrays, numbers, booleans, or null. The keys __proto__, constructor, and prototype are rejected. Visible in dashboard details and explicitly selected, permission-scoped MCP reads; never sent to Slack or Discord.
env string No Lowercase letters, digits, ., _, -, up to 32 characters. Defaults to production; any value you send is lowercased automatically.
occurredAt string (ISO-8601) No Must be within 7 days of the current time. Defaults to the time Releaseo received the event. Send the original timestamp on a retried request — see batching and retries.

Use pending when your backend has reached a step but its terminal outcome is still unknown. For example, a payment worker can report { "step": "payment_authorized", "status": "pending" } before reporting success or failure again with the same ref and step when the outcome is known.

Server-side examples

All examples use the server-only RELEASEO_SERVER_KEY environment variable and the same flat, caller-supplied metadata. Never send raw URL query strings or fragments, email addresses, IP addresses, tokens, or cookies in metadata.

PHP (native cURL)

<?php

$event = [
    'flow' => '0197a1c0-5f6d-7a20-b7c5-2f6f6f6f6f01', 'ref' => $orderId, 'step' => 'started', 'status' => 'success',
    'metadata' => [
        'source' => 'checkout-api', 'referrerHost' => 'app.example.com',
        'referrerPath' => '/checkout', 'user.id' => $userId,
    ],
];

$serverKey = getenv('RELEASEO_SERVER_KEY');
if ($serverKey === false || $serverKey === '') throw new RuntimeException('RELEASEO_SERVER_KEY is required');

$curl = curl_init('https://api.releaseo.io/api/public/flows/events');
curl_setopt_array($curl, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'x-project-key: ' . $serverKey],
    CURLOPT_POSTFIELDS => json_encode($event, JSON_THROW_ON_ERROR),
    CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($curl);
if ($response === false) {
    $message = curl_error($curl);
    curl_close($curl);
    throw new RuntimeException($message);
}
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
if ($status < 200 || $status >= 300) {
    throw new RuntimeException('Releaseo Monitor event failed with HTTP ' . $status);
}

Node.js (fetch)

const serverKey = process.env.RELEASEO_SERVER_KEY;
if (!serverKey) throw new Error("RELEASEO_SERVER_KEY is required");

const response = await fetch(
  "https://api.releaseo.io/api/public/flows/events",
  {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "x-project-key": serverKey,
    },
    body: JSON.stringify({
      flow: "0197a1c0-5f6d-7a20-b7c5-2f6f6f6f6f01",
      ref: orderId,
      step: "started",
      status: "success",
      metadata: {
        source: "checkout-api",
        referrerHost: "app.example.com",
        referrerPath: "/checkout",
        "user.id": userId,
      },
    }),
  },
);
if (!response.ok)
  throw new Error(`Releaseo Monitor event failed: ${response.status}`);

Elixir (Req)

response =
  Req.post!("https://api.releaseo.io/api/public/flows/events",
    headers: [{"x-project-key", System.fetch_env!("RELEASEO_SERVER_KEY")}],
    json: %{
      flow: "0197a1c0-5f6d-7a20-b7c5-2f6f6f6f6f01", ref: order_id, step: "started", status: "success",
      metadata: %{
        "source" => "checkout-api", "referrerHost" => "app.example.com",
        "referrerPath" => "/checkout", "user.id" => user_id
      }
    }
  )

C# (HttpClient)

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;

using var httpClient = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Post, "https://api.releaseo.io/api/public/flows/events");
request.Headers.Add("x-project-key", Environment.GetEnvironmentVariable("RELEASEO_SERVER_KEY")
    ?? throw new InvalidOperationException("RELEASEO_SERVER_KEY is required"));
request.Content = JsonContent.Create(new {
    flow = "0197a1c0-5f6d-7a20-b7c5-2f6f6f6f6f01", @ref = orderId, step = "started", status = "success",
    metadata = new Dictionary<string, string> {
        ["source"] = "checkout-api", ["referrerHost"] = "app.example.com",
        ["referrerPath"] = "/checkout", ["user.id"] = userId,
    },
});
using var response = await httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();

Rust (reqwest)

use serde_json::json;

let response = reqwest::Client::new()
    .post("https://api.releaseo.io/api/public/flows/events")
    .header("x-project-key", std::env::var("RELEASEO_SERVER_KEY")?)
    .json(&json!({
        "flow": "0197a1c0-5f6d-7a20-b7c5-2f6f6f6f6f01", "ref": order_id, "step": "started", "status": "success",
        "metadata": {
            "source": "checkout-api", "referrerHost": "app.example.com",
            "referrerPath": "/checkout", "user.id": user_id,
        }
    }))
    .send()
    .await?;
response.error_for_status()?;

Batching and retries

Send a single object for one event, or a JSON array for up to 100 in one request. Validation runs against the whole batch: the first invalid event — by array position — rejects the entire request with a named error below. Nothing is partially accepted.

A 202 means the batch passed validation and was queued — not that every event is immediately queryable in the dashboard (usually a matter of seconds). Events are accepted at least once: if your backend retries a request after a timeout, send the retried events with the same occurredAt as the original attempt. Releaseo hashes occurredAt into the alert delivery id for step-failed and run-completed alerts, so a retry does not send a second Slack or Discord message for the same event. A duplicate event for the same ref and step is otherwise harmless to the monitor’s health view.

Error codes

Every 400 response is { "code": "...", "message": "..." }. For array input, the message names the offending index (for example, "...at index 1").

Code Meaning
empty_batch The array is empty.
batch_too_large More than 100 events in one request.
unknown_flow flow doesn’t match an enabled monitor in this project.
unknown_step step doesn’t match a declared step on that monitor.
invalid_status status isn’t exactly "success", "failure", or "pending".
missing_ref ref is missing or empty.
ref_too_long ref is over 128 characters.
invalid_error error isn’t a string.
error_too_long error is over 2000 characters.
invalid_message message isn’t a string, or is over 512 characters.
invalid_metadata metadata fails any rule — not an object, over 24 entries, a key that doesn’t match the allowed pattern or is one of the reserved names (__proto__, constructor, prototype), or a value that isn’t a string or is too long.
invalid_env env isn’t a string, or doesn’t match the allowed pattern.
invalid_occurred_at occurredAt isn’t a valid ISO-8601 string.
occurred_at_out_of_range occurredAt is more than 7 days from now.

A null byte anywhere in the request body — inside error, message, a metadata value, or any other string — is rejected before any of the codes above run, with a generic 400 Input contains a forbidden null byte. This is a whole-request input rule, not a named per-field code.

Alerts

Monitor can notify Slack or Discord on two events:

Event Fires Turn it on
Monitor step failed At most once every 15 minutes per step — edge-triggered on a status: "failure" event for that step. Bind a Slack or Discord destination to it.
Monitor run completed Every time a run’s terminal step reports success. Turn on the monitor’s own “Alert when a run completes” switch, and bind a destination.

Both events are opt-in, the same way every other Releaseo event is: open Dashboard → Events → Notification Triggers, find “Monitor step failed” or “Monitor run completed”, and select the Slack channels or Discord destinations that should receive it. Each monitor’s own page also has an Alerts card with a shortcut straight into Notification Triggers, scoped to these two events.

A step can also be muted individually from the monitor’s step editor — a muted step’s failures never alert, and never spend the 15-minute cooldown window.

Folders, owners, and routing

Folders are an operational layer, not just a visual label. They let you group related monitors, assign one project member as the operational owner, and narrow where that folder’s alerts may be delivered. Moving a monitor between folders never changes its integration key or run history.

  • Operational owner — receives the folder’s dashboard failure notifications. If that member loses project access, Releaseo safely falls back to the project’s authorized notification recipients.
  • Inherit project routing — uses the Slack and Discord destinations already selected in Notification Triggers.
  • Only selected destinations — restricts delivery to the folder’s chosen Slack and Discord destinations. It never enables a trigger or bypasses an existing subscription; both layers must allow the event.

If a saved destination becomes unavailable, open Folder settings to repair or remove it. Releaseo preserves that saved choice during unrelated folder edits instead of silently replacing the route.

Custom messages

Both events use the same notification composer as every other Releaseo event trigger. You can:

  • turn the title or description on or off;
  • replace either one with your own static copy (leave the field blank to restore the Releaseo default); and
  • add an optional Additional message with dynamic fields.

Title and description overrides are saved per trigger and destination. If you edit Monitor step failed for two selected Slack channels, for example, the new copy applies to every Monitor step-failed alert sent to those channels — not only to the monitor page from which you opened the dialog.

The Additional message can use these fields:

  • Monitor step failed — flow_name, step_label, error_message, message, run_ref, env
  • Monitor run completed — flow_name, run_ref, env

Test your alert wiring

Every step in a monitor’s funnel has a copy button in the dashboard. The default click copies a ready-to-run success example for that exact step; its menu also offers “Copy failure example” — a ready-to-run curl with a realistic error and message that triggers the real step-failed alert (subject to the 15-minute cooldown) if you run it. Use it to confirm your Slack or Discord destination before relying on it in production.

FAQ

Does the server key expire?

No. It stays valid until you rotate it from Settings → Environment → Server Runtime. Rotating disables the previous key immediately.

What does a run look like when it fails?

Open a run from the failure feed or the recent-runs list to see its timeline. Steps show in the order they occurred; the failed step’s error and message render as-is, plus any metadata you sent, collapsed behind a disclosure once there are more than a few entries.

Do concurrent users create separate runs?

Use a distinct ref for each operation when sending legacy events. Three visitors going through checkout with different labels produce separate timelines in the funnel.

How long is data kept?

90 days, for every field — error, message, and metadata included.

Can staging and production share a monitor?

Yes, with separate env values. Send env: "staging" from your staging backend and it stays out of the dashboard’s default (production) view. env is lowercased automatically and defaults to production when omitted.

Next steps

  • Integrations — connect the Slack or Discord destinations your Notification Triggers deliver to.
  • Backend contract — service ownership and other public API surfaces.
Was this page helpful?