API Documentation

Complete reference for the YouMonit REST API v2, with copy-paste examples in seven languages.

Base URL & conventions

All endpoints are served over HTTPS under the version prefix below. Requests and responses are JSON encoded as UTF-8; timestamps are ISO-8601.

https://api.youmonit.com/api/v2

Authentication

Create an API key in your workspace under Integrations & API, then pass it as a Bearer token on every request. Keys start with ymk_live_ and are shown only once β€” store them safely.

Authorization: Bearer ymk_live_xxxxxxxxxxxxxxxx

Create a key in Integrations & API

Scopes

Each key carries one or more scopes. A write scope implies the matching read scope. A request missing a required scope is rejected with HTTP 403.

Scope Grants
read:monitors Read monitors, their check history and uptime.
write:monitors Create, update, pause and delete monitors.
read:incidents List and read incidents.
write:incidents Acknowledge incidents.
read:status-pages Read public status pages.
read:channels List notification channels.
read:billing Read the subscription, plan limits and invoices.
read:members Read the account profile and its members.
read:devices List push devices the current user has registered.
manage:devices Register / refresh / remove push devices and fire test notifications.
read:updates Long-poll the live incident + monitor status stream (used by the mobile dashboard).
read:support Read your account's YouMonit Support threads and messages.
write:support Open new support threads and reply to existing ones.

Quick start β€” list your monitors

Pick your language above. This example fetches every monitor in the workspace the key belongs to.

curl https://api.youmonit.com/api/v2/monitors \
  -H "Authorization: Bearer ymk_live_xxxxxxxxxxxxxxxx"
<?php
$ch = curl_init('https://api.youmonit.com/api/v2/monitors');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ymk_live_xxxxxxxxxxxxxxxx'],
]);
$body = curl_exec($ch);
curl_close($ch);

$data = json_decode($body, true);
foreach ($data['monitors'] as $m) {
    printf("%s β€” %s\n", $m['name'], $m['last_status']);
}
const res = await fetch('https://api.youmonit.com/api/v2/monitors', {
  headers: { Authorization: 'Bearer ymk_live_xxxxxxxxxxxxxxxx' },
});
const { monitors } = await res.json();
monitors.forEach(m => console.log(`${m.name} β€” ${m.last_status}`));
import requests

res = requests.get(
    "https://api.youmonit.com/api/v2/monitors",
    headers={"Authorization": "Bearer ymk_live_xxxxxxxxxxxxxxxx"},
)
for m in res.json()["monitors"]:
    print(m["name"], "β€”", m["last_status"])
use strict;
use warnings;
use LWP::UserAgent;
use JSON::PP qw(decode_json);

my $ua  = LWP::UserAgent->new;
my $res = $ua->get(
    'https://api.youmonit.com/api/v2/monitors',
    'Authorization' => 'Bearer ymk_live_xxxxxxxxxxxxxxxx',
);
die $res->status_line unless $res->is_success;

my $data = decode_json($res->decoded_content);
for my $m (@{ $data->{monitors} }) {
    printf "%s β€” %s\n", $m->{name}, $m->{last_status};
}
require "net/http"
require "json"

uri = URI("https://api.youmonit.com/api/v2/monitors")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer ymk_live_xxxxxxxxxxxxxxxx"

res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
JSON.parse(res.body)["monitors"].each do |m|
  puts "#{m['name']} β€” #{m['last_status']}"
end
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
)

func main() {
	req, _ := http.NewRequest("GET", "https://api.youmonit.com/api/v2/monitors", nil)
	req.Header.Set("Authorization", "Bearer ymk_live_xxxxxxxxxxxxxxxx")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	var data struct {
		Monitors []struct {
			Name       string `json:"name"`
			LastStatus string `json:"last_status"`
		} `json:"monitors"`
	}
	json.NewDecoder(res.Body).Decode(&data)
	for _, m := range data.Monitors {
		fmt.Printf("%s β€” %s\n", m.Name, m.LastStatus)
	}
}

Create a monitor

POST a JSON body. The fields type, name and target are required; everything else falls back to sensible defaults.

curl -X POST https://api.youmonit.com/api/v2/monitors \
  -H "Authorization: Bearer ymk_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
        "type": "web",
        "name": "Marketing site",
        "target": "https://example.com",
        "interval_seconds": 300
      }'
<?php
$payload = json_encode([
    'type'             => 'web',
    'name'             => 'Marketing site',
    'target'           => 'https://example.com',
    'interval_seconds' => 300,
]);

$ch = curl_init('https://api.youmonit.com/api/v2/monitors');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $payload,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ymk_live_xxxxxxxxxxxxxxxx',
        'Content-Type: application/json',
    ],
]);
$monitor = json_decode(curl_exec($ch), true)['monitor'];
curl_close($ch);
echo "Created monitor #{$monitor['id']}\n";
const res = await fetch('https://api.youmonit.com/api/v2/monitors', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ymk_live_xxxxxxxxxxxxxxxx',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    type: 'web',
    name: 'Marketing site',
    target: 'https://example.com',
    interval_seconds: 300,
  }),
});
const { monitor } = await res.json();
console.log('Created monitor #' + monitor.id);
import requests

res = requests.post(
    "https://api.youmonit.com/api/v2/monitors",
    headers={"Authorization": "Bearer ymk_live_xxxxxxxxxxxxxxxx"},
    json={
        "type": "web",
        "name": "Marketing site",
        "target": "https://example.com",
        "interval_seconds": 300,
    },
)
print("Created monitor #", res.json()["monitor"]["id"])
use strict;
use warnings;
use LWP::UserAgent;
use JSON::PP qw(encode_json decode_json);

my $ua  = LWP::UserAgent->new;
my $res = $ua->post(
    'https://api.youmonit.com/api/v2/monitors',
    'Authorization' => 'Bearer ymk_live_xxxxxxxxxxxxxxxx',
    'Content-Type'  => 'application/json',
    Content         => encode_json({
        type             => 'web',
        name             => 'Marketing site',
        target           => 'https://example.com',
        interval_seconds => 300,
    }),
);
my $monitor = decode_json($res->decoded_content)->{monitor};
print "Created monitor #", $monitor->{id}, "\n";
require "net/http"
require "json"

uri = URI("https://api.youmonit.com/api/v2/monitors")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer ymk_live_xxxxxxxxxxxxxxxx"
req["Content-Type"]  = "application/json"
req.body = {
  type:             "web",
  name:             "Marketing site",
  target:           "https://example.com",
  interval_seconds: 300,
}.to_json

res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts "Created monitor ##{JSON.parse(res.body)['monitor']['id']}"
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
)

func main() {
	payload, _ := json.Marshal(map[string]any{
		"type":             "web",
		"name":             "Marketing site",
		"target":           "https://example.com",
		"interval_seconds": 300,
	})

	req, _ := http.NewRequest("POST",
		"https://api.youmonit.com/api/v2/monitors", bytes.NewReader(payload))
	req.Header.Set("Authorization", "Bearer ymk_live_xxxxxxxxxxxxxxxx")
	req.Header.Set("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)
	defer res.Body.Close()

	var data struct {
		Monitor struct {
			ID int `json:"id"`
		} `json:"monitor"`
	}
	json.NewDecoder(res.Body).Decode(&data)
	fmt.Printf("Created monitor #%d\n", data.Monitor.ID)
}

Monitor object

Every endpoint that returns a monitor emits the same shape. Notable fields:

  • type β€” the monitor type: http, api, tcp, ssh, smtp, pop, imap, ntp, ping, dns, dns_change, whois, domain_expiration, blacklist, ssl_cert, cloudstatus, pagespeed, or push-based heartbeat / webhook / rum.
  • tags β€” array of every tag row attached to the monitor, each with id, slug and name. Empty array when none.
  • last_status β€” online, offline, degraded or unknown.
  • config β€” type-specific settings block (expected status codes, TCP ports, expected DNS answers…). Structure depends on type.
{
  "monitor": {
    "id":               29,
    "name":             "DE1 Jidlo.cz",
    "type":             "web",
    "target":           "https://www.jidlo.cz",
    "port":             null,
    "interval_seconds": 60,
    "timeout_seconds":  30,
    "sensitivity":      1,
    "is_active":        true,
    "is_paused":        false,
    "last_status":      "online",
    "last_checked_at":  "2026-07-28 12:34:07",
    "last_response_ms": 87,
    "config": {
      "method":          "GET",
      "expected_codes":  [200, 301],
      "follow_redirects": true
    },
    "tags": [
      { "id": 4,  "slug": "production", "name": "Production" },
      { "id": 12, "slug": "cs",         "name": "CS" }
    ],
    "created_at": "2026-05-18 15:22:04"
  }
}

Endpoint reference

Monitors

Method Endpoint Scope Description
GET /api/v2/monitors read:monitors Paged list of monitors. Default 20 per page, max 200. Each row includes type, target, live status, config JSON and every tag attached. Optional filters: ?type=http, ?status=offline, ?tag=production. See the Pagination section for the page / per_page / links contract.
POST /api/v2/monitors write:monitors Create a new monitor.
GET /api/v2/monitors/{id} read:monitors Full monitor object including type, target, tags[], config, sensitivity/retries and the last check summary.
PATCH /api/v2/monitors/{id} write:monitors Update fields on an existing monitor.
DELETE /api/v2/monitors/{id} write:monitors Delete a monitor permanently.
GET /api/v2/monitors/{id}/checks read:monitors Paged list of recent check results for one monitor. Default 20 per page, max 100 β€” see the Pagination section below.
GET /api/v2/monitors/{id}/uptime read:monitors Uptime aggregate for 24h / 7d / 30d / 90d.

Incidents

Method Endpoint Scope Description
GET /api/v2/incidents read:incidents Paged incident feed for the account. Default 20 per page, max 100. Optional filters: status, tag, monitor_id. See the Pagination section below.
GET /api/v2/incidents/{id} read:incidents Fetch a single incident by ID.
POST /api/v2/incidents/{id}/acknowledge write:incidents Acknowledge an open incident.

Status pages & channels

Method Endpoint Scope Description
GET /api/v2/status-pages read:status-pages List the workspace status pages.
GET /api/v2/channels read:channels List notification channels.

Account & members

Method Endpoint Scope Description
GET /api/v2/me read:members The account the API key belongs to.
GET /api/v2/accounts/{id} read:members Fetch account details by ID.
GET /api/v2/accounts/{id}/members read:members List members of the account.

Billing

Method Endpoint Scope Description
GET /api/v2/billing/plans β€” Public catalogue of plans β€” no auth required.
GET /api/v2/billing/subscription read:billing Current subscription and plan limits.
GET /api/v2/billing/invoices read:billing Invoice history.

Mobile push (FCM & APNs)

Method Endpoint Scope Description
POST /api/v2/devices manage:devices Register or refresh a mobile push device (upsert on push_token). Body: platform (ios|android|web), push_token, optional device_name / app_version / os_version / timezone / locale.
GET /api/v2/devices read:devices List every push device the calling user has registered β€” includes platform, last_seen_at and any delivery errors.
DELETE /api/v2/devices manage:devices Deregister a device by push_token (soft-delete β€” the record is kept for delivery analytics).
POST /api/v2/devices/test-push manage:devices Send a test push to every active device the calling user owns. Useful during onboarding to verify FCM/APNs credentials are wired up.

Live dashboard (long-poll)

Method Endpoint Scope Description
GET /api/v2/updates read:updates Long-poll delta stream of incident lifecycle events + monitor status flips since a cursor. Optional wait=<seconds> parks the request server-side until fresh data arrives (up to 25 s). Response includes a new cursor to pass on the next call.

Support inbox

Method Endpoint Scope Description
GET /api/v2/support/threads read:support List every support thread in this account, newest activity first. Includes an unread count for badge rendering.
POST /api/v2/support/threads write:support Open a new thread. Body: { subject, body }. Triggers a notification email to YouMonit Support.
GET /api/v2/support/threads/{id} read:support Full thread payload with every message + any attached action buttons.
POST /api/v2/support/threads/{id}/reply write:support Post a reply. Body: { body }.
POST /api/v2/support/threads/{id}/read write:support Mark this thread read from the user side (clears the unread badge in /app and the API list count).

Pagination

List endpoints that can return more than a page's worth of data are paginated. Pass page (1-indexed) and per_page as query params. The response includes a pagination object and a links object with absolute URLs so clients can walk the pages without rebuilding query strings themselves.

EndpointDefault per_pageMax per_page
GET /api/v2/monitors 20200
GET /api/v2/monitors/{id}/checks 20100
GET /api/v2/incidents 20100

Request

GET /api/v2/incidents?page=2&per_page=20&status=open
Authorization: Bearer ymk_live_xxxxxxxxxxxxxxxx

The legacy limit parameter is still accepted as an alias for per_page so pre-pagination clients keep working, but new code should use per_page.

Response shape

{
  "incidents": [ /* 20 items on page 2, newest first */ ],
  "pagination": {
    "page":         2,
    "per_page":     20,
    "total":        137,
    "total_pages":  7,
    "has_more":     true
  },
  "links": {
    "first": "https://api.youmonit.com/api/v2/incidents?status=open&page=1&per_page=20",
    "prev":  "https://api.youmonit.com/api/v2/incidents?status=open&page=1&per_page=20",
    "next":  "https://api.youmonit.com/api/v2/incidents?status=open&page=3&per_page=20",
    "last":  "https://api.youmonit.com/api/v2/incidents?status=open&page=7&per_page=20"
  }
}
  • pagination.total_pages = ceil(total / per_page). Zero when there's no data.
  • pagination.has_more is a convenience β€” page < total_pages.
  • links.next and links.prev are null when there is no next / previous page. Walk the list by following next until it's null.
  • All filter query params are preserved on the returned links, so paging through a filtered set is trivial.

Follow the next link

# Bash β€” page through every open incident, 20 at a time
url="https://api.youmonit.com/api/v2/incidents?status=open&per_page=20"
while [ -n "$url" ] && [ "$url" != "null" ]; do
  resp=$(curl -s -H "Authorization: Bearer ymk_live_…" "$url")
  echo "$resp" | jq '.incidents[] | "\(.id)  \(.monitor_name)  \(.first_error)"'
  url=$(echo "$resp" | jq -r '.links.next')
done
// JavaScript β€” same idea, using fetch()
async function walkIncidents(apiKey) {
  let url = 'https://api.youmonit.com/api/v2/incidents?status=open&per_page=20';
  while (url) {
    const res = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
    const data = await res.json();
    data.incidents.forEach(i => console.log(i.id, i.monitor_name, i.first_error));
    url = data.links.next;   // null on the last page β†’ loop exits
  }
}

Errors & status codes

Errors return the appropriate HTTP status with a JSON body containing an error field describing what went wrong.

200 / 201Success β€” the response body carries the resource.
401Missing, malformed or revoked Bearer token.
403The token is valid but lacks the required scope.
404The resource does not exist or is not in your account.
422Validation failed β€” check the error message for the offending field.
429Rate limit exceeded β€” slow down and retry.
{ "error": "token lacks required scope: write:monitors" }

Rate limits

API keys are held to a fair-use budget of 120 requests per minute. Batch where you can; exceeding the budget returns HTTP 429.

Outgoing webhooks

Instead of polling, register a webhook in Integrations & API to receive monitor.up and monitor.down events. Each delivery is signed with HMAC-SHA-256 so you can verify it came from YouMonit.

Mobile push notifications

The YouMonit mobile app receives incident alerts through Firebase Cloud Messaging (Android / web builds) and Apple Push Notification service (iOS). Registering a device is a one-call setup β€” from that moment on the user automatically receives push notifications for every monitor in the account, with no Contact configuration needed.

Each notification payload includes event, monitor_id, incident_id, target and http_code in its data block so the app can deep-link straight to the incident detail.

1 Β· Register the device

Call POST /api/v2/devices once, right after the OS grants push permission and the FCM/APNs SDK hands you a token. The endpoint is idempotent β€” call it again on every app launch with the current token and it will upsert / reactivate the record without creating duplicates.

curl -X POST https://api.youmonit.com/api/v2/devices \
  -H "Authorization: Bearer ymk_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
        "platform":     "ios",
        "push_token":   "d5f2a7...c9",
        "device_name":  "Petr's iPhone 15",
        "app_version":  "1.4.2",
        "os_version":   "iOS 18.2",
        "timezone":     "Europe/Prague",
        "locale":       "en"
      }'
<?php
$ch = curl_init('https://api.youmonit.com/api/v2/devices');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => json_encode([
        'platform'    => 'android',
        'push_token'  => $fcmToken,
        'device_name' => 'Pixel 8',
        'app_version' => '1.4.2',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ymk_live_xxxxxxxxxxxxxxxx',
        'Content-Type: application/json',
    ],
]);
$device = json_decode(curl_exec($ch), true)['device'];
curl_close($ch);
echo "Registered device #{$device['id']} for push\n";
// React Native / Expo / Capacitor: call this after the OS grants push
// permission and the SDK hands you a fresh token.
await fetch('https://api.youmonit.com/api/v2/devices', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ymk_live_xxxxxxxxxxxxxxxx',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    platform:   Platform.OS,        // 'ios' | 'android'
    push_token: fcmOrApnsToken,
    device_name: Device.modelName,
    app_version: Application.nativeApplicationVersion,
    os_version:  Device.osVersion,
    timezone:    Intl.DateTimeFormat().resolvedOptions().timeZone,
    locale:      Localization.locale,
  }),
});
import requests

requests.post(
    "https://api.youmonit.com/api/v2/devices",
    headers={"Authorization": "Bearer ymk_live_xxxxxxxxxxxxxxxx"},
    json={
        "platform":   "android",
        "push_token": fcm_token,
        "device_name": "Pixel 8",
    },
)
use LWP::UserAgent;
use JSON::PP qw(encode_json);

my $ua = LWP::UserAgent->new;
$ua->post(
    'https://api.youmonit.com/api/v2/devices',
    'Authorization' => 'Bearer ymk_live_xxxxxxxxxxxxxxxx',
    'Content-Type'  => 'application/json',
    Content => encode_json({
        platform    => 'android',
        push_token  => $fcm_token,
        device_name => 'Pixel 8',
    }),
);
require "net/http"
require "json"

uri = URI("https://api.youmonit.com/api/v2/devices")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer ymk_live_xxxxxxxxxxxxxxxx"
req["Content-Type"]  = "application/json"
req.body = { platform: "android", push_token: fcm_token, device_name: "Pixel 8" }.to_json

Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
package main

import (
	"bytes"
	"encoding/json"
	"net/http"
)

func RegisterDevice(apiKey, fcmToken string) error {
	body, _ := json.Marshal(map[string]any{
		"platform":    "android",
		"push_token":  fcmToken,
		"device_name": "Pixel 8",
	})
	req, _ := http.NewRequest("POST",
		"https://api.youmonit.com/api/v2/devices", bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Content-Type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	res.Body.Close()
	return nil
}

That's it. Once a device is registered, its owner automatically receives push notifications for every monitor in the account β€” no Contact wiring required. Add a push method to a Contact only when you want a different team member to receive alerts for a specific monitor.

Token gone stale? YouMonit auto-detects Unregistered / BadDeviceToken (APNs) and UNREGISTERED / NOT_FOUND (FCM) responses and soft-deletes the device row. Re-registering on next launch reactivates it silently.

2 Β· Live dashboard (long-poll)

Push handles background delivery. When the app is in the foreground it should long-poll GET /api/v2/updates to receive the same events at zero latency without a WebSocket. Pass the last cursor you received; add wait=25 to have the server park the request until fresh data arrives (or 25 s pass, whichever is first).

# `since` is the cursor from the previous response; `wait=25` parks the
# request server-side until fresh data lands (or 25 s elapse).
curl "https://api.youmonit.com/api/v2/updates?since=1721984561000&wait=25" \
  -H "Authorization: Bearer ymk_live_xxxxxxxxxxxxxxxx"
<?php
$cursor = 0;   // start from now; persist between calls
while (true) {
    $url = 'https://api.youmonit.com/api/v2/updates?wait=25&since=' . $cursor;
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 35,
        CURLOPT_HTTPHEADER     => ['Authorization: Bearer ymk_live_xxxxxxxxxxxxxxxx'],
    ]);
    $data = json_decode(curl_exec($ch), true);
    curl_close($ch);

    foreach ($data['incidents'] as $i) {
        printf("[%s] %s β€” %s\n", $i['status'], $i['monitor_name'], $i['first_error']);
    }
    $cursor = $data['cursor'];
}
// Simple recursive long-poll loop for the mobile dashboard.
let cursor = 0;
async function pollUpdates() {
  const res = await fetch(`https://api.youmonit.com/api/v2/updates?wait=25&since=${cursor}`, {
    headers: { Authorization: 'Bearer ymk_live_xxxxxxxxxxxxxxxx' },
  });
  const data = await res.json();
  cursor = data.cursor;
  data.incidents.forEach(i => renderIncident(i));
  data.monitor_updates.forEach(m => refreshTile(m));
  setTimeout(pollUpdates, 0);   // immediate re-poll
}
pollUpdates();
import requests, time

cursor = 0
while True:
    r = requests.get(
        "https://api.youmonit.com/api/v2/updates",
        params={"since": cursor, "wait": 25},
        headers={"Authorization": "Bearer ymk_live_xxxxxxxxxxxxxxxx"},
        timeout=35,
    )
    data = r.json()
    cursor = data["cursor"]
    for i in data["incidents"]:
        print(i["status"], i["monitor_name"], i.get("first_error", ""))
use LWP::UserAgent;
use JSON::PP qw(decode_json);

my $ua = LWP::UserAgent->new(timeout => 35);
my $cursor = 0;
while (1) {
    my $res = $ua->get(
        "https://api.youmonit.com/api/v2/updates?wait=25&since=$cursor",
        'Authorization' => 'Bearer ymk_live_xxxxxxxxxxxxxxxx',
    );
    my $data = decode_json($res->decoded_content);
    $cursor = $data->{cursor};
    for my $i (@{ $data->{incidents} }) {
        printf "[%s] %s β€” %s\n", $i->{status}, $i->{monitor_name}, $i->{first_error};
    }
}
require "net/http"
require "json"

cursor = 0
loop do
  uri = URI("https://api.youmonit.com/api/v2/updates?wait=25&since=#{cursor}")
  req = Net::HTTP::Get.new(uri)
  req["Authorization"] = "Bearer ymk_live_xxxxxxxxxxxxxxxx"
  res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: 35) { |h| h.request(req) }
  data = JSON.parse(res.body)
  cursor = data["cursor"]
  data["incidents"].each { |i| puts "#{i['status']} #{i['monitor_name']}" }
end
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"time"
)

type update struct {
	Cursor    int64 `json:"cursor"`
	Incidents []struct {
		Status      string `json:"status"`
		MonitorName string `json:"monitor_name"`
		FirstError  string `json:"first_error"`
	} `json:"incidents"`
}

func main() {
	client := &http.Client{Timeout: 35 * time.Second}
	cursor := int64(0)
	for {
		req, _ := http.NewRequest("GET",
			fmt.Sprintf("https://api.youmonit.com/api/v2/updates?wait=25&since=%d", cursor), nil)
		req.Header.Set("Authorization", "Bearer ymk_live_xxxxxxxxxxxxxxxx")
		res, err := client.Do(req)
		if err != nil { continue }

		var u update
		json.NewDecoder(res.Body).Decode(&u)
		res.Body.Close()
		cursor = u.Cursor
		for _, i := range u.Incidents {
			fmt.Printf("[%s] %s β€” %s\n", i.Status, i.MonitorName, i.FirstError)
		}
	}
}

3 Β· Test end-to-end

Call POST /api/v2/devices/test-push during onboarding to fire a notification to every device the user has registered. Useful when validating your FCM service-account JSON and APNs .p8 key are configured on the platform.

curl -X POST https://api.youmonit.com/api/v2/devices/test-push \
  -H "Authorization: Bearer ymk_live_xxxxxxxxxxxxxxxx"

YouMonit Support (in-app messaging)

Every account has a support inbox reachable at /app/support in the web UI and at /api/v2/support/threads via the API. Threads are conversations between the account's users and YouMonit Support. Every new message fires an email notification to the counterparty, and admin replies can attach one or more action buttons that render as clickable CTAs both in the email and inside the app (e.g. "Pay subscription" β†’ /app/billing).

Open a thread + reply

# Open a new support thread
curl -X POST https://api.youmonit.com/api/v2/support/threads \
  -H "Authorization: Bearer ymk_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
        "subject": "Billing question β€” annual switch",
        "body":    "Hi, we\'d like to move to annual billing starting next month."
      }'

# Reply on an existing thread
curl -X POST https://api.youmonit.com/api/v2/support/threads/17/reply \
  -H "Authorization: Bearer ymk_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "body": "Thanks β€” please also enable the invoice PDF option." }'

# Mark a thread read from the user side
curl -X POST https://api.youmonit.com/api/v2/support/threads/17/read \
  -H "Authorization: Bearer ymk_live_xxxxxxxxxxxxxxxx"

Thread + message shape

{
  "thread": {
    "id":              17,
    "subject":         "Billing question β€” annual switch",
    "status":          "open",
    "opened_by_admin": false,
    "unread":          false,
    "last_message_at": "2026-07-28 14:22:07",
    "created_at":      "2026-07-28 09:11:02"
  },
  "messages": [
    {
      "id":           41,
      "thread_id":    17,
      "from_admin":   false,
      "sender_email": "billing@customer.com",
      "body":         "Hi, we'd like to move to annual billing.",
      "actions":      [],
      "created_at":   "2026-07-28 09:11:02",
      "read_at":      "2026-07-28 09:14:38"
    },
    {
      "id":           42,
      "thread_id":    17,
      "from_admin":   true,
      "sender_email": null,
      "body":         "Sure β€” jump to Billing to switch cadence anytime.",
      "actions": [
        {
          "label":       "API keys",
          "url":         "/app/integrations",
          "full_url":    "https://www.youmonit.com/app/integrations",
          "style":       "primary",
          "is_internal": true
        },
        {
          "label":       "Pay subscription",
          "url":         "/app/billing",
          "full_url":    "https://www.youmonit.com/app/billing",
          "style":       "primary",
          "is_internal": true
        }
      ],
      "created_at":   "2026-07-28 14:22:07",
      "read_at":      null
    }
  ]
}

from_admin: true = message written by YouMonit Support. read_at is stamped when the other side opens the thread β€” use it to render "read" ticks in a chat UI.

Action buttons on messages

When an admin attaches action buttons to their reply (e.g. "Pay subscription", "API keys"), each button surfaces in actions[] on the message. Mobile clients and third-party integrations can render these as tappable buttons β€” the API gives you everything needed to display and dispatch each one:

FieldTypeMeaning
labelstringText on the button (e.g. API keys).
urlstringURL as stored. Usually a relative platform path like /app/integrations.
full_urlstringAlways an absolute URL. Relative paths get prefixed with https://www.youmonit.com. Pass this to WebView / browser openers directly.
stylestringprimary / secondary / danger β€” drives the button colour to match the web rendering.
is_internalbooltrue when the URL points at youmonit.com. Mobile apps use this to route the click into an in-app WebView; false means "open in the system browser".

The web app renders each action as a Bootstrap button using the style field. Reproduce the same look on mobile / in another client by matching classes or your platform's equivalent primary / secondary / danger button treatments:

// Web / JS β€” mirrors the button styling used inside /app/support
function styleClass(style) {
  return style === 'danger'    ? 'btn btn-sm btn-outline-danger'
       : style === 'secondary' ? 'btn btn-sm btn-outline-secondary'
       :                          'btn btn-sm btn-primary';
}
function renderAction(a) {
  return '<a href="' + a.url + '" class="' + styleClass(a.style) + '">'
       +    a.label
       + '</a>';
}
// e.g. { label: "API keys", url: "/app/integrations", style: "primary" }
// β†’   <a href="/app/integrations" class="btn btn-sm btn-primary">API keys</a>
// React Native β€” dispatch to WebView for in-app links, system browser otherwise
import { Linking, TouchableOpacity, Text } from 'react-native';

function ActionButton({ action, onOpenInApp }) {
  const bg = action.style === 'danger'    ? '#dc2626'
           : action.style === 'secondary' ? '#334155'
           :                                 '#4CAF87';
  return (
    <TouchableOpacity
      onPress={() => action.is_internal
        ? onOpenInApp(action.full_url)
        : Linking.openURL(action.full_url)}
      style={{ backgroundColor: bg, padding: 10, borderRadius: 8 }}
    >
      <Text style={{ color: '#fff', fontWeight: '600' }}>{action.label}</Text>
    </TouchableOpacity>
  );
}