Skip to main content
publish apiSDKroot bootstraprequest formatsdisplay rulesmultipart

Publish API

Send one notification request, derive one public channel code, and hand users a web channel URL or bootstrap response.

Docs version
SDK 0.3.x Current
01 · Overview

Smallest useful mental model

Keep the workflow tight: publish key in, request out, channel code derived, channel page shared.

02 · Quick start

First successful publish

curl -X POST https://alrim.io/<publishKey> \
  -H 'Content-Type: application/json' \
  -H 'X-Alrim-Severity: warning' \
  -H 'X-Alrim-Ring: force' \
  -H 'X-Alrim-Level: 2' \
  -d '{
    "title": "Server deploy",
    "body": "v1.2.3 shipped",
    "data": {
      "service": "api"
    }
  }'
Read in order
  1. 1. Paste a publishKey between 32 and 256 characters using only lowercase letters and digits.
  2. 2. Start with the path route so the key and payload stay visible in one request.
  3. 3. Open the derived channel page after success to confirm delivery.
03 · Authentication

Path auth first

Path-based: POST /<publishKey> is the most direct route when you already know the publish key. Use POST /<publishKeyA>,<publishKeyB> to send the same payload to multiple derived channels in one request.

Header-based fan-out: POST /api/notify also accepts comma-separated keys in X-Alrim-Key. Keep publish keys out of the request body; the body is always the notification payload.

Root route: POST / generates a random publish key and returns it alongside the send result. GET / returns a CLI bootstrap envelope only for curl-like user agents.

Content type: the parser follows the request content-type. Use JSON, form-urlencoded, plain text, or multipart/form-data; curl --data-urlencode and -d send form-urlencoded by default.

Validation: trimmed keys must be at least 32, at most 256, and use only lowercase letters a-z plus digits 0-9.

04 · Publish routes

Path, multi-target, and root route examples

POST /<publishKey>

Accepts JSON, form-urlencoded, plain text, or multipart uploads. Plain text uses the raw request body as body, and ?title=... can supply the notification title when the payload does not already include one.

curl -X POST https://alrim.io/<publishKey> \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Smoke test",
    "body": "publish key in path"
  }'
POST /<publishKeyA>,<publishKeyB>

A comma-separated publish key list fans out one request to multiple channels. Each key is validated and hashed independently, duplicate derived channels are rejected, and the body remains the notification payload. A request can target up to 50 keys; multipart fan-out with uploaded files is limited to 5 targets.

Multi path
curl -X POST https://alrim.io/<publishKeyA>,<publishKeyB> \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Server deploy",
    "body": "send the same payload to two channels"
  }'
POST /api/notify with X-Alrim-Key

The compatibility header route supports the same comma-separated key list via X-Alrim-Key: keyA,keyB. Single-key requests keep the existing direct response shape; comma-separated requests return a batch response.

Header fan-out
curl -X POST https://alrim.io/api/notify \
  -H 'X-Alrim-Key: <publishKeyA>,<publishKeyB>' \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Server deploy",
    "body": "send the same payload to two channels"
  }'
Selected request format

JSON body with an explicit application/json content-type header.

Selected format
curl -X POST https://alrim.io/<publishKey> \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Smoke test",
    "body": "publish key in path"
  }'
POST /<publishKey> with attachments

Attach files with repeated files fields. All file extensions are accepted; clients may show a warning before opening executable, script, web, and app package formats. Uploaded bytes are hosted by alrim storage; FCM receives only attachment metadata and public URLs.

Multipart
curl -X POST https://alrim.io/<publishKey>   -H 'X-Alrim-Severity: warning'   -H 'X-Alrim-Level: 2'   -F 'title=Incident report'   -F 'body=Screenshots and runbook attached'   -F 'data={"service":"incident-api"}'   -F 'files=@./screenshot.png'   -F 'files=@./runbook.pdf'
POST /

Generates a random publish key, sends the notification once, and returns both the send result and the generated publish URL.

curl -X POST https://alrim.io/ \
  -H 'Content-Type: application/json' \
  --data-binary '{"title":"Server deploy","body":"v1.2.3 shipped"}'
GET / (CLI only)

When the request user agent looks like a CLI tool such as curl, the root GET route returns a bootstrap JSON payload with a generated publish key and ready-to-copy curl commands.

Root GET
curl https://alrim.io/
05 · Request body

Payload shape

Keep the top level small: title, body, and optional publisher-owned data. Alrim controls are sent outside the request body.

Payload
{
  "title": "Server deploy",
  "body": "v1.2.3 shipped",
  "data": {
    "service": "api",
    "deployId": "v1.2.3"
  }
}
URL options
curl -X POST "https://alrim.io/<publishKey>?title=Server%20deploy&severity=warning&show=1&ring=force&level=2"   -H 'Content-Type: text/plain'   --data-binary 'v1.2.3 shipped'
Header controls
curl -X POST https://alrim.io/<publishKey>   -H 'Content-Type: application/json'   -H 'X-Alrim-Severity: warning'   -H 'X-Alrim-Show: 1'   -H 'X-Alrim-Ring: force'   -H 'X-Alrim-Level: 2'   -d '{"title":"Server deploy","body":"v1.2.3 shipped"}'
Field notes

Auto injected: channel_code, alrim_created_at_ms, notification_id, and optional alrim_controls metadata are added to the FCM transport envelope. Your request body stays unchanged.

Form fields: nested publisher data accepts both data[service]=api and data.service=api. Duplicate field names are rejected.

Plain text: text/plain maps the entire request body to body. Add ?title=... to produce { title, body: rawText } without sending JSON.

Publisher data: data is never interpreted by Alrim. Legacy data.severity, data.show, data.ring, and data.level no longer affect delivery behavior.

Alrim controls: send severity, show, ring, and level as X-Alrim-* headers, or as query parameters for curl/manual calls. Query title becomes the payload title only when the payload has no title. Sending the same control in both query and header is rejected.

Attachments: use multipart/form-data and repeated files fields. External attachment URLs are not accepted; alrim hosts uploaded files and returns metadata in attachments. The policy is permissive: upload all extensions, avoid SVG image previews, and warn clients before opening executable/script-like formats.

FCM limit: the encoded data payload is rejected before delivery when it exceeds the server safety budget of about 3800 bytes.

06 · Display Rules

Template notification titles and bodies

Display Rules are client-side templates saved per channel. They do not change the stored send payload or the FCM request; they only change how alrim renders the notification title and body in the web inbox and channel history.

Input payload
{
  "title": "Deploy",
  "body": "v1.2.3 shipped",
  "data": {
    "service": "api",
    "status": "open",
    "tags": ["release", "backend"],
    "meta": {
      "region": "iad"
    }
  }
}
Templates
Title template:
{{ data.service | upper }} - {{ title | default: "Untitled" }}

Body template:
{{ body | default: data.message | truncate: 120 }}
{{ data.tags | join: ", " | prepend: "tags: " }}
{{ data.status | eq: "open" | if: "needs attention", "resolved" }}
Rendered output
API - Deploy

v1.2.3 shipped
tags: release, backend
needs attention
Expression syntax

Wrap each expression in {{ ... }}. Plain text outside expressions is kept as-is.

Start with a path such as title, body, data.service, data.meta.region, or data.tags.0. Use $ for the whole template input object.

Chain functions with pipes: {{ data.service | trim | upper }}. Function arguments come after a colon and multiple arguments are comma-separated: {{ status | eq: "open" | if: "Open", "Closed" }}.

Arguments can be quoted strings, numbers, booleans, null, or another path. String escapes supported inside quotes are \\n, \\r, \\t, \\', \\", and \\\\.

Templates are limited to 240 characters. Rendered titles are capped at 240 characters and rendered bodies at 2000.

Value functions
FunctionArgsBehavior
trimnoneRemove leading and trailing whitespace.
lower, lowercasenoneLowercase text.
upper, uppercasenoneUppercase text.
capitalizenoneTrim, then uppercase the first character.
truncatenKeep the first n characters.
defaultvalueUse value only when the current value is null or missing.
append, concatvalueAdd text after the current value.
prependvalueAdd text before the current value.
replacefrom, toReplace exact text matches.
Logic and collection functions
FunctionArgsBehavior
containsvalueCase-insensitive substring check.
eqvalueExact text equality.
nevalueExact text inequality.
ifyes, noReturn yes when the current value is truthy, otherwise no.
lengthnoneString or array length, object key count, or text length for other displayable values.
joinseparatorJoin array items with separator.
jsonnoneStringify the current value as compact JSON.
prettynoneRender objects/arrays as indented JSON with 2-space formatting; strings and primitive values pass through as text.

Unknown paths, invalid expressions, or unknown functions render as empty text.

Common templates

{{ title }} keeps the sent title.

{{ body | default: $ | pretty }} keeps the sent body, then falls back to the whole input object as readable JSON.

{{ data.service | upper }} renders a nested data value.

{{ data.tags | join: ", " }} renders an array as text.

{{ data.status | eq: "open" | if: "Open", "Closed" }} maps a boolean expression to labels.

{{ data.meta | json }} renders an object as compact JSON.

{{ $ | pretty }} renders the full payload as indented JSON; string values pass through without extra quotes.

07 · Responses

Response envelopes

POST /<publishKey> returns the direct notify result. Comma-separated publish keys return a batch envelope with one result per target and use HTTP 207 when only some targets fail. POST / adds generated publish key metadata on top of the send result. GET / for CLI user agents returns only the generated publish key bootstrap payload.

POST /<publishKey> success
Success
{
  "ok": true,
  "channel_code": "3ba3f5f43b92602683c19aee62a20342",
  "channel_url": "https://alrim.me/3ba3f5f43b92602683c19aee62a20342",
  "notification_id": "d6d5f95b3bbd4fb4b8d98f7bc8e89c17",
  "created_at_ms": 1712345678901,
  "attachments": []
}
Comma-separated publish keys success
Batch success
{
  "ok": true,
  "total": 2,
  "sent": 2,
  "failed": 0,
  "results": [
    {
      "index": 0,
      "ok": true,
      "status": 200,
      "body": {
        "ok": true,
        "channel_code": "3ba3f5f43b92602683c19aee62a20342",
        "channel_url": "https://alrim.me/3ba3f5f43b92602683c19aee62a20342",
        "notification_id": "d6d5f95b3bbd4fb4b8d98f7bc8e89c17",
        "created_at_ms": 1712345678901,
        "attachments": []
      }
    },
    {
      "index": 1,
      "ok": true,
      "status": 200,
      "body": {
        "ok": true,
        "channel_code": "e704fc0d4f7f8f0f4ff3f2ea1a9ee5c8",
        "channel_url": "https://alrim.me/e704fc0d4f7f8f0f4ff3f2ea1a9ee5c8",
        "notification_id": "afd6728f7be84082922553cc4ca1363b",
        "created_at_ms": 1712345678901,
        "attachments": []
      }
    }
  ]
}
POST / failure
Error
{
  "ok": false,
  "error": "publishKey must use only lowercase letters (a-z) and digits (0-9). See https://alrim.io/doc for publishKey rules and examples."
}
POST / success with generated publish key
Root POST response
{
  "ok": true,
  "publish_key": "5765aff90d6e4d2e169b3b496c418cc9",
  "publish_url": "https://alrim.io/5765aff90d6e4d2e169b3b496c418cc9",
  "channel_code": "3ba3f5f43b92602683c19aee62a20342",
  "channel_url": "https://alrim.me/3ba3f5f43b92602683c19aee62a20342",
  "short_code": "123-456",
  "short_code_expires_at_ms": 1712346578901,
  "short_code_notice": "Short code valid for 15 minutes.",
  "notification_id": "d6d5f95b3bbd4fb4b8d98f7bc8e89c17",
  "created_at_ms": 1712345678901,
  "curl": "curl -X POST "https://alrim.io/5765aff90d6e4d2e169b3b496c418cc9" ..."
}
GET / CLI bootstrap response
Root GET response
{
  "ok": true,
  "publish_key": "5765aff90d6e4d2e169b3b496c418cc9",
  "publish_url": "https://alrim.io/5765aff90d6e4d2e169b3b496c418cc9",
  "channel_code": "3ba3f5f43b92602683c19aee62a20342",
  "channel_url": "https://alrim.me/3ba3f5f43b92602683c19aee62a20342",
  "short_code": "123-456",
  "short_code_expires_at_ms": 1712346578901,
  "short_code_notice": "Short code valid for 15 minutes.",
  "curl": "curl -X POST "https://alrim.io/5765aff90d6e4d2e169b3b496c418cc9" ...",
  "curl_json": "curl -X POST "https://alrim.io/5765aff90d6e4d2e169b3b496c418cc9" ...",
  "curl_form": "curl -X POST "https://alrim.io/5765aff90d6e4d2e169b3b496c418cc9" ...",
  "curl_text": "curl -X POST "https://alrim.io/5765aff90d6e4d2e169b3b496c418cc9" ..."
}
08 · SDK

Server keys, public subscribe UI

@alrim-me/sdk/server now uses a channel-first API. Create a publisher with a private secret, call channel(name), and publish with channel.publish(payload, options). Single-channel sends use POST /<publishKey>; channel arrays fan out through POST /api/notify with a comma-separated X-Alrim-Key header. Browser widgets receive only public channel config.

Migration notes

createAlrimClient() becomes createAlrimPublisher().

scope becomes a private channel name passed to publisher.channel('deploys').

publishNotification(...) becomes channel.publish(payload, options); controls and attachments live in the second argument.

createSubscribeConfig(...) becomes channel.subscribeConfig(...). The returned config is public and safe to pass to browser code.

Next.js route handler
channel-first
import { createAlrimPublisher } from '@alrim-me/sdk/server';

const alrim = createAlrimPublisher({
  secret: process.env.ALRIM_SECRET!
});
const deploys = alrim.channel('deploys');

export async function POST() {
  await deploys.publish(
    {
      title: 'Server deploy',
      body: 'v1.2.3 shipped',
      data: { service: 'api' }
    },
    {
      controls: { severity: 'warning', ring: 'force', level: 2 }
    }
  );

  return Response.json({
    subscribe: deploys.subscribeConfig({
      title: 'Deploys'
    })
  });
}
Multi-channel publish
channel array
const releaseChannels = alrim.channel(['deploys', 'incidents']);

await releaseChannels.publish({
  title: 'Server deploy',
  body: 'v1.2.3 shipped'
});

// The SDK derives one publish key per channel name and sends:
// POST /api/notify
// X-Alrim-Key: <deploysKey>,<incidentsKey>
// Body remains the notification payload only.
// Fan-out channels are publish-only; use alrim.channel('name') for subscribe config.
Direct publish keys
createAlrimChannel
import { createAlrimChannel } from '@alrim-me/sdk/server';

const alerts = createAlrimChannel({
  publishKey: process.env.ALRIM_PUBLISH_KEY!
});

await alerts.publish({
  title: 'Direct publish',
  body: 'Using an existing publish key'
});

const fanout = createAlrimChannel({
  publishKey: [process.env.ALERTS_KEY!, process.env.INCIDENTS_KEY!]
});

await fanout.publish({ title: 'Fan-out', body: 'Sent to two publish keys' });
File attachments
multipart
await alrim.channel('incidents').publish(
  {
    title: 'Incident report',
    body: 'Screenshots and runbook attached',
    data: { incidentId: 'inc_123' }
  },
  {
    attachments: [
      chartFile,
      { name: 'runbook.txt', content: 'restart api workers', type: 'text/plain' },
      { name: 'payload.json', content: JSON.stringify({ incidentId: 'inc_123' }), type: 'application/json' }
    ]
  }
);

// Attachments switch the request to multipart/form-data.
// Pass File/Blob objects directly, or { name, content, type } on the server.
// The JSON payload is sent in the data field; files are repeated files fields.
Publish errors
AlrimPublishError
import { AlrimPublishError } from '@alrim-me/sdk/server';

try {
  await alrim.channel('deploys').publish({ title: 'Ping' });
} catch (error) {
  if (error instanceof AlrimPublishError) {
    console.error(error.status, error.body);
  }
}
SvelteKit server load
Server only
import { createAlrimPublisher } from '@alrim-me/sdk/server';

const alrim = createAlrimPublisher({
  secret: process.env.ALRIM_SECRET!
});
const deploys = alrim.channel('deploys');

export const load = () => ({
  subscribe: deploys.subscribeConfig({
    title: 'Deploys'
  })
});
Nuxt server handler
Server only
import { createAlrimPublisher } from '@alrim-me/sdk/server';

const alrim = createAlrimPublisher({
  secret: process.env.ALRIM_SECRET!
});

export default defineEventHandler(async () => {
  const deploys = alrim.channel('deploys');

  await deploys.publish({
    title: 'Server deploy'
  });

  return {
    subscribe: deploys.subscribeConfig({
      title: 'Deploys'
    })
  };
});
Express
Server only
import express from 'express';
import { createAlrimPublisher } from '@alrim-me/sdk/server';

const app = express();
const alrim = createAlrimPublisher({
  secret: process.env.ALRIM_SECRET!
});
const deploys = alrim.channel('deploys');

app.post('/deploy', express.json(), async (_req, res) => {
  await deploys.publish({ title: 'Server deploy' });
  res.json({ ok: true });
});

app.get('/subscribe-config', (_req, res) => {
  res.json(deploys.subscribeConfig({
    title: 'Deploys'
  }));
});
CDN subscribe widget
Pinned v1
<script src="https://alrim.me/sdk/v1/alrim-subscribe.js"></script>

<alrim-me
  channel-code="3ba3f5f43b92602683c19aee62a20342"
  title="Deploys"
  origin="https://alrim.me"
></alrim-me>