# Talarius API

API base URL: `https://talarius.io/api`

## What Talarius does

Send app alerts to a paired phone or live browser display.

Talarius sends short app alerts to a paired iPhone or Android phone or a live browser display. It is for solo developers and small teams running 1–10 low-volume applications. It is not a full observability suite.

There are two ways to send an alert:

- Notifications — your code tells Talarius that something needs attention.
- Heartbeats — a service checks in while healthy; Talarius alerts when check-ins stop or resume.

> **INFO:** The production Go API supports browser, APNs, and FCM notification delivery, heartbeat monitoring, reveal-once API keys, iOS and Android pairing, metadata-only usage history, and Stripe-hosted prepaid credit purchases.

### API base URL

```text
https://talarius.io/api
```

### Next steps

Sign in, create a project, copy its reveal-once initial API key, then pair an iOS or Android phone or open a browser display.

---

## Quickstart

Send your first alert in under two minutes.

You'll create a project, choose where the alert should appear, and send one notification.

### 1. Create a project

From the dashboard, create a project. You receive a reveal-once API key named Initial. Copy it now — it is shown only once.

### 2. Add a receiver

Pair an iOS or Android phone with the 15-minute code from the project’s Devices panel. For the fastest first test, create a named browser display instead, copy its reveal-once URL, and open it in the browser you will leave on screen. Keep codes and display URLs private.

### 3. Send a notification

```bash
curl -X POST https://talarius.io/api/v1/notify \
  -H "Authorization: Bearer $TALARIUS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: deploy-482" \
  -d '{"title":"Deployment failed","body":"Production exited with **status 1**.\n\n[Open logs](https://example.com/logs/42)","bodyFormat":"markdown"}'
```

A successful request returns 202 Accepted with the target count and one credit charged.

During a short dependency interruption, ordinary API requests may wait up to ten seconds for recovery. A 503 response includes Retry-After guidance. Preserve the original Idempotency-Key when safely retrying a notification; do not blindly repeat a mutation with an uncertain outcome.

```json
{
  "status": "accepted",
  "notificationId": "notification_01JZ8R6GXN8V5D7WQ2Y4K1M3PA",
  "targets": 1,
  "creditsCharged": 1
}
```

### Delivery results

`GET /api/v1/notifications/{notificationID}`

Use an active API key for the same project to read the queued, processing, completed, failed, or expired state for up to 15 minutes. Results contain target counts and aggregate provider outcomes, never the notification title or body. A provider acceptance is not a device presentation receipt.

> **TIP:** One accepted alert costs one credit regardless of whether it reaches one device or ten.

---

## Add Talarius with Codex

Copy a focused prompt and let Codex adapt the integration to your project.

These recipes ask Codex to inspect your existing stack, preserve its conventions, and add Talarius without putting a project key in browser code or making alert delivery part of the critical path.

### Alert after a background job fails

Notify only after the job has exhausted its normal retries.

```text
Inspect this repository's background-job and error-handling conventions. Add a small server-side Talarius notification client for https://talarius.io/api/v1/notify and call it only after a job exhausts its existing retries. Use a concise, non-sensitive title and body. Derive a stable Idempotency-Key from the job name and run ID so the same logical failure cannot be billed twice. Treat HTTP 202 as accepted without claiming device delivery. Use TALARIUS_API_KEY only from the server or CI environment. Add the variable to .env.example without a value. Do not expose the API key to browser code, commit it, or log it. Do not include secrets, credentials, personal, confidential, regulated, or other sensitive information in notification payloads or copied logs. Send only the minimum operational context and use non-secret identifiers or access-controlled HTTPS links without embedded credentials. Use a bounded timeout, keep Talarius failures non-fatal to the host workflow, and add focused tests with a mocked HTTP server—never a live key.
```

### Alert when a deployment fails

Send one concise alert from the final failure path in CI.

```text
Inspect this repository's CI and deployment workflow. On the final failed-deployment path, send a server-side POST to https://talarius.io/api/v1/notify with a concise project and failure summary. Read TALARIUS_API_KEY from the CI secret store and use the CI run or deployment ID as a stable Idempotency-Key. Do not echo credentials, raw exception dumps, or sensitive response content. Treat HTTP 202 as accepted without claiming device delivery. Use TALARIUS_API_KEY only from the server or CI environment. Add the variable to .env.example without a value. Do not expose the API key to browser code, commit it, or log it. Do not include secrets, credentials, personal, confidential, regulated, or other sensitive information in notification payloads or copied logs. Send only the minimum operational context and use non-secret identifiers or access-controlled HTTPS links without embedded credentials. Use a bounded timeout, keep Talarius failures non-fatal to the host workflow, and add focused tests with a mocked HTTP server—never a live key.
```

### Alert when a payment workflow needs attention

Notify when the existing payment workflow reaches a final actionable failure.

```text
Inspect this repository's server-side payment workflow and its existing retry and reconciliation behavior. On the final actionable failure path, send a POST to https://talarius.io/api/v1/notify with a concise title and only a non-secret payment reference or access-controlled HTTPS link. Never include card data, customer details, provider secrets, or raw webhook content. Derive a stable Idempotency-Key from the workflow and non-secret event reference. Treat HTTP 202 as accepted without claiming device delivery. Use TALARIUS_API_KEY only from the server or CI environment. Add the variable to .env.example without a value. Do not expose the API key to browser code, commit it, or log it. Do not include secrets, credentials, personal, confidential, regulated, or other sensitive information in notification payloads or copied logs. Send only the minimum operational context and use non-secret identifiers or access-controlled HTTPS links without embedded credentials. Use a bounded timeout, keep Talarius failures non-fatal to the host workflow, and add focused tests with a mocked HTTP server—never a live key.
```

### Add a service heartbeat

Let Talarius detect a missed check-in and later recovery.

```text
Inspect this repository for the existing server-side scheduler, health loop, or worker lifecycle. Add an empty-body POST to https://talarius.io/api/v1/heartbeat once per minute while the service is healthy. Treat HTTP 204 as accepted. Do not send a request body or call the endpoint from browser code. Avoid overlapping heartbeat requests and do not crash or stall the service when Talarius is unavailable. Use TALARIUS_API_KEY only from the server or CI environment. Add the variable to .env.example without a value. Do not expose the API key to browser code, commit it, or log it. Do not include secrets, credentials, personal, confidential, regulated, or other sensitive information in notification payloads or copied logs. Send only the minimum operational context and use non-secret identifiers or access-controlled HTTPS links without embedded credentials. Use a bounded timeout, keep Talarius failures non-fatal to the host workflow, and add focused tests with a mocked HTTP server—never a live key.
```

---

## Authentication

Project-scoped bearer API keys.

Every production notification and heartbeat request authenticates with a project-scoped API key passed as a bearer token.

```http
Authorization: Bearer $TALARIUS_API_KEY
```

### Key rules

- Keys are reveal-once — copy the secret when it is created.
- A project allows two active keys.
- Rotation is staged: the old key remains active until you revoke it.
- Revoked key metadata stays visible but cannot authenticate.

> **WARNING:** Never commit API keys to source control. Store them as environment variables or in a secrets manager.

### Authentication errors

| Name | Type | Description |
| --- | --- | --- |
| 401 | unauthorized | Missing, malformed, invalid, or revoked bearer key. |
| 402 | payment_required | The account effective balance is zero or negative. |

---

## Notifications

Send an important event to a project's configured devices.

`POST /api/v1/notify`

Queues a title-and-body alert for every configured browser display and paired phone. Eligible iOS and Android phones receive independent native-push and foreground SSE attempts; an open stream never suppresses APNs or FCM. Talarius persists service data only for as long as necessary for essential operations. An unstarted delivery expires after its 15-minute processing window, but logical expiry is not a physical-erasure guarantee. Native apps deduplicate by notification ID and store their own local history.

> **WARNING:** Do not include secrets, credentials, personal, confidential, regulated, or other sensitive information in notification titles or bodies. Send only the minimum operational context and use non-secret identifiers or access-controlled HTTPS links without embedded credentials.

### Request body

| Name | Type | Description |
| --- | --- | --- |
| title * | string | Required. Up to 100 Unicode characters. |
| body * | string | Required. Up to 1,000 Unicode characters. |
| bodyFormat | plain_text \| markdown | Optional. Defaults to plain_text. |
| Idempotency-Key | header | Optional project-scoped retry key. |

### Markdown bodies

Set bodyFormat to markdown to format the notification body. Titles are always plain text, and bodies default to literal plain text when bodyFormat is omitted.

- Text — paragraphs, headings, bold, italic, and strikethrough.
- Lists and structure — ordered, unordered, and task lists; blockquotes; and thematic breaks.
- Code and data — inline code, fenced code blocks, and GitHub-Flavored Markdown tables.
- Links — absolute HTTPS links and autolinks.

> **INFO:** Raw HTML displays literally. Images never load and show their alt text. Unsupported formatting stays readable, and only absolute HTTPS links with a hostname are interactive. Source Markdown counts toward the 1,000-character and combined 2,400-byte limits.

```json
{
  "title": "Deployment failed",
  "body": "Production exited with **status 1**.\n\n[Open logs](https://example.com/logs/42)",
  "bodyFormat": "markdown"
}
```

### Examples

#### cURL

```curl
curl -X POST https://talarius.io/api/v1/notify \
  -H "Authorization: Bearer $TALARIUS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: deploy-482" \
  -d '{"title":"Deployment failed","body":"Production exited with **status 1**.\n\n[Open logs](https://example.com/logs/42)","bodyFormat":"markdown"}'
```

#### JavaScript

```javascript
const apiKey = process.env.TALARIUS_API_KEY;
if (!apiKey) throw new Error('TALARIUS_API_KEY is required');

const response = await fetch('https://talarius.io/api/v1/notify', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + apiKey,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'deploy-482'
  },
  body: JSON.stringify({
    title: 'Deployment failed',
    body: 'Production exited with **status 1**.\n\n[Open logs](https://example.com/logs/42)',
    bodyFormat: 'markdown'
  }),
  signal: AbortSignal.timeout(10_000)
});

if (response.status !== 202) {
  throw new Error('Talarius returned ' + response.status);
}
```

#### Python

```python
import os
import requests

response = requests.post(
    'https://talarius.io/api/v1/notify',
    headers={
        'Authorization': f"Bearer {os.environ['TALARIUS_API_KEY']}",
        'Idempotency-Key': 'deploy-482',
    },
    json={
        'title': 'Deployment failed',
        'body': 'Production exited with **status 1**.\n\n[Open logs](https://example.com/logs/42)',
        'bodyFormat': 'markdown',
    },
    timeout=10,
)
if response.status_code != 202:
    raise RuntimeError(f'Talarius returned {response.status_code}')
```

#### Go

```go
package main

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"net/http"
	"os"
	"time"
)

func main() {
	if err := notifyTalarius(context.Background()); err != nil {
		log.Fatal(err)
	}
}

func notifyTalarius(ctx context.Context) error {
	apiKey := os.Getenv("TALARIUS_API_KEY")
	if apiKey == "" {
		return errors.New("TALARIUS_API_KEY is required")
	}
	payload, err := json.Marshal(map[string]string{
		"title":      "Deployment failed",
		"body":       "Production exited with **status 1**.\n\n[Open logs](https://example.com/logs/42)",
		"bodyFormat": "markdown",
	})
	if err != nil {
		return err
	}
	ctx, cancel := context.WithTimeout(ctx, 10 * time.Second)
	defer cancel()
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://talarius.io/api/v1/notify", bytes.NewReader(payload))
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Idempotency-Key", "deploy-482")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusAccepted {
		return fmt.Errorf("Talarius returned %s", resp.Status)
	}
	return nil
}
```

### Idempotency and limits

For 24 hours, a matching replay returns the original 202 response without charging again. Reusing the same key for another payload returns 409. Per-project rate limits reject excess alerts with 429 and do not bill them. A project with no configured devices returns 409 without charge; once a device is configured, an alert is accepted and charged even if every display is offline.

---

## Heartbeats

Detect a missed application check-in and notify again when it recovers.

`POST /api/v1/heartbeat`

Send one empty-body check-in per minute from a job or service. The first accepted ping activates monitoring. A missed timeout creates one outage alert; the next accepted ping creates one recovery alert.

```bash
curl -X POST https://talarius.io/api/v1/heartbeat \
  -H "Authorization: Bearer $TALARIUS_API_KEY"
```

An accepted ping returns 204 and is free. It requires a positive account balance and at least one configured browser or iOS device. Timeout and recovery alerts each cost one credit.

### Timeouts and states

| Name | Type | Description |
| --- | --- | --- |
| inactive | state | No accepted check-in has activated monitoring yet. |
| healthy | state | The latest accepted check-in is within the configured timeout. |
| down | state | A deadline was missed and one outage event was generated. |

> **WARNING:** Ten pings may be accepted in a rolling minute. The eleventh starts a fixed one-hour penalty with one accepted ping per rolling minute; rejected pings do not refresh liveness.

---

## Devices

Use a browser display or pair an iOS or Android phone for focused alerts.

A project supports up to ten active devices shared across browser displays and paired phones. Each browser display is a separate page with one current display session.

### Secret links

- The full URL is revealed only when a device is created or its link is rotated.
- Its secret stays in the URL fragment, is scrubbed from the address bar, and is exchanged for a scoped HTTP-only session cookie.
- The link is reusable until rotation or revocation; opening it replaces the previous display session for that device.
- The authenticated `/api/projects/{id}/devices/events` stream is a metadata-free SSE invalidation stream. Clients reload device lists after `ready` or `devices-changed`; events are not replayed.

### Delivery model

Delivery uses a live-only Server-Sent Events stream with no replay. The page shows the latest alert prominently and keeps up to 50 newer-first alerts in memory for the current browser session; reload clears that feed.

> **INFO:** The dashboard reports current connected/offline state and last-seen time. Rotating or revoking a link immediately terminates the open display.

### Paired iPhones

- Create a reveal-once eight-character code from the project Devices panel; it expires after 15 minutes.
- Enter the code in the iOS 17 app. Pairing consumes the code and creates a device-only credential stored in Keychain.
- While the app is active, one foreground SSE connection covers every active project pairing. It is live-only with no replay, reconnects with bounded backoff, and is not reported as online presence.
- The app opens `/api/mobile/installations/events` with its installation bearer; a newer connection replaces the older one and events are never replayed.
- APNs and the live stream are two transports for one logical iPhone. When notification permission is denied, APNs is skipped but the foreground monitor remains available.
- The app's user-visible notification history and read state stay on that iPhone. Unpairing retains local history while stopping future delivery.
- A project supports ten active devices shared across browser displays and phones.

### Paired Android phones

Install the Talarius Android app on an Android 10 or newer phone with Google Play services, generate a 15-minute pairing code in the project Devices panel, and enter it manually. Pairing creates no Talarius account or purchase flow in the app.

> **INFO:** FCM and the live stream are two transports for one logical Android phone. Notification permission controls title-only background system alerts; denial does not block pairing or foreground monitoring while Talarius is open. Local history remains on that phone until the person deletes it.

---

## Credits & billing

Prepaid, account-wide notification usage.

Credits share one account wallet while every notification debit records its consuming project.

### Welcome credits

New accounts receive 100 credits automatically. The grant is applied once and expires after one calendar year.

### Buy credit packs

Buy 1,000 credits for $1 USD or 10,000 credits for $5 USD through Stripe Checkout. These are one-time, tax-inclusive purchases, and each pack expires after one calendar year.

### Track usage

Usage returns searchable account and project metadata. Notification titles and bodies are not part of usage records or responses.

### How charging works

- One accepted alert equals one credit regardless of target count.
- Configured devices count as targets even when their display pages are offline.
- Matching idempotent retries are never charged twice.
- Credits nearest expiration are used first, and nonzero expirations appear in Usage.
- Rejected validation, no-device, authentication, and rate-limit requests are not billed.
- The fixed dashboard test alert is free, limited to one per project every ten seconds, and creates no ledger entry.
- Its short-lived delivery result distinguishes none, skipped, disabled, accepted, partial, and failed native outcomes. mobileDelivery retains the iOS/APNs counters; androidDelivery separately exposes targets, fcmEligible, fcmSkipped, accepted, failed, and liveTargeted. Provider acceptance and stream enqueue are not phone presentation receipts.

> **TIP:** Checkout is hosted by Stripe. Talarius grants credits only from verified provider events; refunds and disputes reverse proportional credits and can leave a negative effective balance.

---

## Errors & rate limits

The stable API error contract.

Errors return a JSON envelope with an HTTP status and machine-readable code.

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Project notification rate limit exceeded"
  }
}
```

### Status codes

| Name | Type | Description |
| --- | --- | --- |
| 202 | accepted | Notification accepted for delivery. |
| 204 | no_content | Heartbeat or deletion accepted. |
| 400 | bad_request | Invalid JSON, input, or body shape. |
| 401 | unauthorized | Missing or invalid credentials. |
| 402 | payment_required | The effective prepaid balance is zero or negative. |
| 409 | conflict | Idempotency, missing-device, or capacity conflict. |
| 429 | rate_limited | Configured project or free-test limit exceeded; not billed. |

---

## Complete HTTP endpoint catalog

- `GET /api/auth/session` — Read the current session
- `GET /api/auth/github/start` — Start GitHub OAuth with state and PKCE
- `GET /api/auth/github/callback` — Complete GitHub OAuth and create a dashboard session
- `POST /api/auth/magic-link` — Request a single-use email sign-in link
- `GET /api/auth/magic-link/consume` — Validate and stage a magic link without consuming it
- `POST /api/auth/magic-link/confirm` — Consume a staged magic link and create a session
- `POST /api/auth/logout` — Revoke the current session
- `DELETE /api/account` — Permanently delete an account and revoke access
- `GET /api/overview` — Read dashboard overview
- `GET /api/usage` — List metadata-only account usage history
- `GET /api/wallet` — Read the account wallet, catalog, usage, and purchases
- `POST /api/wallet/checkout-sessions` — Create a Stripe-hosted one-time Checkout session
- `GET /api/wallet/purchases/{purchaseId}` — Read an owned credit purchase
- `GET /api/wallet/purchases/{purchaseId}/events` — Observe an owned credit purchase until it leaves pending state
- `GET /api/projects` — List owned projects
- `POST /api/projects` — Create a project and reveal its initial API key once
- `GET /api/projects/{id}` — Read one owned project
- `DELETE /api/projects/{id}` — Soft-delete one owned project and revoke its API keys
- `POST /api/projects/{projectID}/keys` — Create and reveal an API key once
- `PATCH /api/projects/{projectID}/heartbeat` — Update the missing-heartbeat timeout
- `GET /api/projects/{projectID}/uptime` — Read owner-only heartbeat observed-uptime history
- `PATCH /api/projects/{projectID}/rate-limits` — Update notification rate limits
- `POST /api/projects/{projectID}/keys/{keyID}/revoke` — Revoke an API key
- `GET /api/projects/{projectID}/virtual-devices` — List browser virtual devices and live connection state
- `POST /api/projects/{projectID}/virtual-devices` — Create a virtual device and reveal its reusable secret URL once
- `DELETE /api/projects/{projectID}/virtual-devices/{deviceID}` — Revoke a virtual device and disconnect its display
- `POST /api/projects/{projectID}/virtual-devices/{deviceID}/rotate` — Rotate a virtual-device link and disconnect the current display
- `GET /api/projects/{projectID}/mobile-invites` — List phone pairing-invite metadata without revealing codes
- `POST /api/projects/{projectID}/mobile-invites` — Create a reveal-once phone pairing code valid for 15 minutes
- `DELETE /api/projects/{projectID}/mobile-invites/{inviteID}` — Revoke an unused phone pairing invite
- `GET /api/projects/{projectID}/mobile-devices` — List paired native phones
- `DELETE /api/projects/{projectID}/mobile-devices/{deviceID}` — Revoke a paired native phone
- `GET /api/projects/{projectID}/devices/events` — Observe owned project device and pairing-state changes
- `POST /api/projects/{projectID}/test-alerts` — Send the fixed, unbilled dashboard test alert
- `GET /api/projects/{projectID}/delivery-results/{notificationID}` — Read a short-lived delivery result for an owned project
- `GET /api/projects/{projectID}/delivery-results/{notificationID}/events` — Observe a short-lived delivery result
- `POST /api/virtual-devices/{deviceID}/session` — Exchange the URL-fragment secret for a scoped HTTP-only device session
- `GET /api/virtual-devices/{deviceID}/events` — Open the live-only virtual-device SSE stream
- `POST /api/mobile/pairings` — Consume a pairing code and register a native phone
- `PUT /api/mobile/installations/registration` — Refresh native registration for every project paired to one installation
- `GET /api/mobile/installations/events` — Open the installation-wide live native monitor
- `DELETE /api/mobile/devices/{deviceID}` — Unpair the credential holder's native phone
- `POST /api/v1/notify` — Send an alert to every configured device in the API key's project
- `GET /api/v1/notifications/{notificationID}` — Read a short-lived delivery result for the API key's project
- `POST /api/v1/heartbeat` — Record a project heartbeat

## Canonical resources

- [Web documentation](https://talarius.io/docs)
- [OpenAPI specification](https://talarius.io/openapi.yaml)
