hubbub v0.1.0

https://notify.rlew.io

One JSON API in, delivery to the operator's channels out.

Callers are machines, so every outcome is explicit enough to branch on: the HTTP status carries the verdict and result repeats it in the body. A 202 is a promise, not a receipt — the message is durably queued and its outcome is written to the hub's delivery log later, keyed by the requestId this call returned.

Authentication is a bearer key. The key's channel permission list is its delivery set: omit channels and the notification goes to every channel the key permits. Supplying channels narrows that set and can never widen it — naming a channel the key lacks is a 403, not a silent intersection.

Delivery is at-least-once: a caller that times out and retries may double-send.

POST /v1/notify bearer key required

Send a notification to the caller's channels.

Enqueues the notification for every target channel, waits out the server's response window, and reports what is known by then: attempts that finished are reported concretely, anything still in flight as queued.

Request body

FieldTypeRules
title* string ≤ 256 bytes Required. Max 256 bytes after sanitisation. All control characters are stripped — titles can land in HTTP headers and email subjects.
message* string ≤ 4096 bytes Required. Max 4096 bytes after sanitisation. Newlines and tabs survive (multi-line log excerpts are content); other control characters are stripped. Adapters truncate further to fit the channel. Required even alongside html — it is the plain-text alternative, and the only thing a text-only channel can show.
channels string[] email | ntfy Optional subset of the key's permitted channels. Omit it to use the key's full set. An explicit empty array is rejected with 400 rather than treated as absent. The enum on items lists the channel ids this deployment has configured; a key still only reaches the ones it is permitted.
html string ≤ 131072 bytes Optional rich body for channels that can render one — email today, and ignored by every text-only channel. Send a fragment (<p>done</p>) to get it wrapped in hubbub's styled mail shell, or a whole document (starting <!doctype or <html) to own the markup completely. Passed through as written: control characters are stripped and nothing else is, so treat the ability to post here as the ability to put arbitrary markup in the operator's inbox.
priority string low | default | high | urgent · default: default Display only — never changes which channels fire. Omitted or empty means default; parsing is case-insensitive.
tags string[] ≤ 16 items · each ≤ 64 bytes Short labels passed to adapters. Control characters are stripped and tags left empty are dropped.

Try it

Sending delivers a real notification to this hub's channels, writes a line to the delivery log, and spends one of the hourly rate cap.

Responses

  • 200 Delivered: every target channel completed successfully inside the response window.
    example body
    {
      "channels": {
        "ntfy": "ok"
      },
      "requestId": "r_cc519c9a3ee79f270c4e9823cd56f2f3",
      "result": "delivered"
    }
  • 202 Queued: nothing finished inside the response window. The message is durably spooled and will be retried; the outcome lands in the delivery log under this requestId.
    example body
    {
      "channels": {
        "ntfy": "queued"
      },
      "requestId": "r_cc519c9a3ee79f270c4e9823cd56f2f3",
      "result": "queued"
    }
  • 207 Partial: channels disagreed — some combination of delivered, queued, failed or disabled. Inspect channels per id.
    example body
    {
      "channels": {
        "ntfy": "ok",
        "parked": "disabled"
      },
      "requestId": "r_cc519c9a3ee79f270c4e9823cd56f2f3",
      "result": "partial"
    }
  • 400 Malformed request: bad JSON, an unknown field, an unparseable priority, an over-length body, or "channels": [] (omit the field instead — an empty list is a caller that computed no targets, and must not fan out to everything).
  • 401 Missing or unknown bearer key.
  • 403 The request named a channel this key is not permitted to use. A channel the key is permitted but which is currently disabled is not a 403 — it comes back as a per-channel disabled result.
  • 429 Global rate cap reached. Not a 202: a cap has to look like an error so standard retry machinery honours it rather than reading it as success.
  • 502 Every target channel failed permanently.

Response body

FieldTypeRules
result* string delivered | queued | partial | failed The verdict, repeating what the status code already said. Branch on the status code first and this second — never infer either from the channel map.
requestId* string Correlation id, also written to the hub's delivery log. Quote it when reporting a problem, and use it to find the settled outcome of anything reported as queued.
channels* object Per-channel outcome, keyed by channel id.

GET /docs no auth

Browsable API reference.

This document rendered as a page, with a panel for firing a real request against this hub. Generated from the spec on every load rather than maintained beside it, so it cannot describe an endpoint that isn't here.

Try it

Responses

  • 200 The reference page.

GET /health no auth

Liveness.

200 if the process is serving. Liveness only — no outbound probes and no channel state, so a healthy response says nothing about whether deliveries are succeeding.

Try it

Responses

  • 200 The process is up.
    example body
    {
      "status": "ok",
      "uptime": "3h14m0s",
      "version": "0.1.0"
    }

Response body

FieldTypeRules
status* string always ok
version* string
uptime* string Go duration string, e.g. "3h14m0s".

GET /llms.txt no auth

This contract, condensed, as markdown.

The llms.txt convention (llmstxt.org): what this hub is and how to call it, written for an agent that was handed the host and nothing else. Unauthenticated, and a strict subset of this document — read the spec instead if you can parse it.

Try it

Responses

  • 200 Markdown, served as text/plain so it renders wherever it is fetched.

GET /openapi.json no auth

This document.

Unauthenticated by design: it exposes the API's shape, which is not a secret, so an agent can be pointed at the base URL and discover the contract before it holds a key. The channels enum and the server URL reflect this deployment.

Try it

Responses

  • 200 The OpenAPI document.

GET / no auth

Human-readable landing page.

A short HTML page for a person who opened the base URL: what this is, one copy-pasteable request, and the response contract. Also served at /index.html. It documents the API and never the deployment behind it, so it names no channel, caller or ops endpoint.

Try it

Responses

  • 200 The landing page.