curl --request POST \
--url https://api.reflecto.dev/v1/send \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"message": "Backup finished in 12m",
"title": "Backup",
"priority": "default",
"tags": [
"ops"
],
"url": "https://example.com/backup/123",
"url_title": "View report",
"actions": [
{
"label": "Acknowledge",
"url": "https://example.com/ack"
}
],
"device": "all",
"markdown": false,
"ttl": 259200
}
'import requests
url = "https://api.reflecto.dev/v1/send"
payload = {
"message": "Backup finished in 12m",
"title": "Backup",
"priority": "default",
"tags": ["ops"],
"url": "https://example.com/backup/123",
"url_title": "View report",
"actions": [
{
"label": "Acknowledge",
"url": "https://example.com/ack"
}
],
"device": "all",
"markdown": False,
"ttl": 259200
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
message: 'Backup finished in 12m',
title: 'Backup',
priority: 'default',
tags: ['ops'],
url: 'https://example.com/backup/123',
url_title: 'View report',
actions: [{label: 'Acknowledge', url: 'https://example.com/ack'}],
device: 'all',
markdown: false,
ttl: 259200
})
};
fetch('https://api.reflecto.dev/v1/send', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.reflecto.dev/v1/send",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'message' => 'Backup finished in 12m',
'title' => 'Backup',
'priority' => 'default',
'tags' => [
'ops'
],
'url' => 'https://example.com/backup/123',
'url_title' => 'View report',
'actions' => [
[
'label' => 'Acknowledge',
'url' => 'https://example.com/ack'
]
],
'device' => 'all',
'markdown' => false,
'ttl' => 259200
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.reflecto.dev/v1/send"
payload := strings.NewReader("{\n \"message\": \"Backup finished in 12m\",\n \"title\": \"Backup\",\n \"priority\": \"default\",\n \"tags\": [\n \"ops\"\n ],\n \"url\": \"https://example.com/backup/123\",\n \"url_title\": \"View report\",\n \"actions\": [\n {\n \"label\": \"Acknowledge\",\n \"url\": \"https://example.com/ack\"\n }\n ],\n \"device\": \"all\",\n \"markdown\": false,\n \"ttl\": 259200\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.reflecto.dev/v1/send")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"message\": \"Backup finished in 12m\",\n \"title\": \"Backup\",\n \"priority\": \"default\",\n \"tags\": [\n \"ops\"\n ],\n \"url\": \"https://example.com/backup/123\",\n \"url_title\": \"View report\",\n \"actions\": [\n {\n \"label\": \"Acknowledge\",\n \"url\": \"https://example.com/ack\"\n }\n ],\n \"device\": \"all\",\n \"markdown\": false,\n \"ttl\": 259200\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.reflecto.dev/v1/send")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"message\": \"Backup finished in 12m\",\n \"title\": \"Backup\",\n \"priority\": \"default\",\n \"tags\": [\n \"ops\"\n ],\n \"url\": \"https://example.com/backup/123\",\n \"url_title\": \"View report\",\n \"actions\": [\n {\n \"label\": \"Acknowledge\",\n \"url\": \"https://example.com/ack\"\n }\n ],\n \"device\": \"all\",\n \"markdown\": false,\n \"ttl\": 259200\n}"
response = http.request(request)
puts response.read_body{
"id": "msg_3f6e2a7c9b1d4f8e8a5b6c1d2e3f4a5b",
"delivered_to": [
{
"device_id": "dev_9af3",
"type": "android",
"delivery": "push"
},
{
"device_id": "dev_2bc1",
"type": "extension",
"delivery": "push"
}
],
"warnings": []
}Send a notification to your paired devices
Sends a notification to one or every paired device for the user behind the bearer token. Accepts JSON, form-encoded, or text/plain bodies. When the body is text/plain (or has no Content-Type), the entire body becomes the message field.
Total serialized envelope size is hard-capped at 2048 bytes. Requests over that limit return 413 payload_too_large before any rate-limit counters are charged.
Unknown device labels in the device parameter are surfaced as warnings, not errors — long-running cron jobs do not break when a device is unpaired.
curl --request POST \
--url https://api.reflecto.dev/v1/send \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"message": "Backup finished in 12m",
"title": "Backup",
"priority": "default",
"tags": [
"ops"
],
"url": "https://example.com/backup/123",
"url_title": "View report",
"actions": [
{
"label": "Acknowledge",
"url": "https://example.com/ack"
}
],
"device": "all",
"markdown": false,
"ttl": 259200
}
'import requests
url = "https://api.reflecto.dev/v1/send"
payload = {
"message": "Backup finished in 12m",
"title": "Backup",
"priority": "default",
"tags": ["ops"],
"url": "https://example.com/backup/123",
"url_title": "View report",
"actions": [
{
"label": "Acknowledge",
"url": "https://example.com/ack"
}
],
"device": "all",
"markdown": False,
"ttl": 259200
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
message: 'Backup finished in 12m',
title: 'Backup',
priority: 'default',
tags: ['ops'],
url: 'https://example.com/backup/123',
url_title: 'View report',
actions: [{label: 'Acknowledge', url: 'https://example.com/ack'}],
device: 'all',
markdown: false,
ttl: 259200
})
};
fetch('https://api.reflecto.dev/v1/send', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.reflecto.dev/v1/send",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'message' => 'Backup finished in 12m',
'title' => 'Backup',
'priority' => 'default',
'tags' => [
'ops'
],
'url' => 'https://example.com/backup/123',
'url_title' => 'View report',
'actions' => [
[
'label' => 'Acknowledge',
'url' => 'https://example.com/ack'
]
],
'device' => 'all',
'markdown' => false,
'ttl' => 259200
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.reflecto.dev/v1/send"
payload := strings.NewReader("{\n \"message\": \"Backup finished in 12m\",\n \"title\": \"Backup\",\n \"priority\": \"default\",\n \"tags\": [\n \"ops\"\n ],\n \"url\": \"https://example.com/backup/123\",\n \"url_title\": \"View report\",\n \"actions\": [\n {\n \"label\": \"Acknowledge\",\n \"url\": \"https://example.com/ack\"\n }\n ],\n \"device\": \"all\",\n \"markdown\": false,\n \"ttl\": 259200\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.reflecto.dev/v1/send")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"message\": \"Backup finished in 12m\",\n \"title\": \"Backup\",\n \"priority\": \"default\",\n \"tags\": [\n \"ops\"\n ],\n \"url\": \"https://example.com/backup/123\",\n \"url_title\": \"View report\",\n \"actions\": [\n {\n \"label\": \"Acknowledge\",\n \"url\": \"https://example.com/ack\"\n }\n ],\n \"device\": \"all\",\n \"markdown\": false,\n \"ttl\": 259200\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.reflecto.dev/v1/send")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"message\": \"Backup finished in 12m\",\n \"title\": \"Backup\",\n \"priority\": \"default\",\n \"tags\": [\n \"ops\"\n ],\n \"url\": \"https://example.com/backup/123\",\n \"url_title\": \"View report\",\n \"actions\": [\n {\n \"label\": \"Acknowledge\",\n \"url\": \"https://example.com/ack\"\n }\n ],\n \"device\": \"all\",\n \"markdown\": false,\n \"ttl\": 259200\n}"
response = http.request(request)
puts response.read_body{
"id": "msg_3f6e2a7c9b1d4f8e8a5b6c1d2e3f4a5b",
"delivered_to": [
{
"device_id": "dev_9af3",
"type": "android",
"delivery": "push"
},
{
"device_id": "dev_2bc1",
"type": "extension",
"delivery": "push"
}
],
"warnings": []
}Authorizations
Opaque bearer token created on your paired Android phone. The format is rfk_live_ followed by 32 URL-safe alphanumeric characters (≥160 bits of entropy). Test tokens use the rfk_test_ prefix.
Body
Canonical send payload. Form-encoded and text/plain bodies are documented in the guides — the server normalises them to this shape.
Notification body as plain text — write quotes and newlines literally, never JSON-escape them (no " or \n sequences; the JSON layer handles encoding). Required. 1–1500 bytes (UTF-8). maxLength is omitted from the schema because JSON Schema measures characters, not bytes — server enforces a byte cap to keep the 2 KB envelope budget honest across CJK / emoji content.
1"Backup finished in 12m"
Headline rendered above the body. 1–100 bytes (UTF-8). Schema omits maxLength because JSON Schema measures characters, not bytes.
1"Backup"
Notification priority. min/low use normal FCM; rest use high.
min, low, default, high, urgent "default"
Up to 5 tags rendered as labels. Oversize / overcount entries are truncated with a warning rather than rejected — maxItems is deliberately omitted so SDK generators don't client-side-reject valid-but-overcount inputs that the server will accept and trim.
Tag string. Oversize / overcount entries are truncated with a warning rather than rejected.
Clickable URL attached to the notification. http(s) only, ≤ 512 bytes.
"https://example.com/backup/123"
Display text for url. ≤ 32 bytes (UTF-8). Schema omits maxLength because JSON Schema measures characters, not bytes.
"View report"
Up to 3 action buttons. Each button opens its url when tapped. Overcount entries are truncated with a warning — maxItems is deliberately omitted so SDK generators don't client-side-reject.
Show child attributes
Show child attributes
Target device(s). all (default), mobile (alias phone), pc (every computer — browser extension, web app, CLI and desktop app), desktop (the native desktop app only), a comma-separated list of device labels, or an exact device id. Labels are not unique — one shared by two devices delivers to both, so pass an id to reach exactly one. all, mobile, phone, pc and desktop are reserved: a device you have NAMED after one of them is not reachable by that name, only by its id. An unknown label becomes a warning and falls back to delivering everywhere; a reserved word that matches none of your devices delivers to nobody and says so in warnings.
256"all"
Render the body as Markdown on the extension. Ignored on Android in MVP.
false
Seconds the message stays in the per-device queue if not delivered live. Out-of-range values are clamped to the [0, 259200] range (72 hours, matching the Redis queue TTL) and a warning is returned.
0 <= x <= 259200259200