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": "checkout-payment",
"ref": "req_8f3a",
"step": "started",
"status": "success",
"metadata": {
"source": "checkout-api",
"referrerHost": "app.example.com",
"referrerPath": "/checkout",
"user.id": "user_42"
}
}'
ref identifies one execution of the monitor — your own request id, order id,
or job id works well. Send every step of that execution with the same
ref; Releaseo groups them into a single run.
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.
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": "checkout-payment",
"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 | string | Yes | Technical key of an enabled monitor in this project. |
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 only in the dashboard’s run details and timeline; 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' => 'checkout-payment', '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: "checkout-payment",
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: "checkout-payment", 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 = "checkout-payment", @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": "checkout-payment", "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?
Yes. Each ref is its own run. Three visitors going through checkout at the
same time produce three refs and three independent 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.
Thanks — your feedback helps us improve the docs.