# Talarius API

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

## Introduction

Focused application alerts on browser displays and paired iPhones.

Talarius turns important application events into focused alerts on a browser display or paired iPhone. It's built for solo developers and small teams running 1–10 low-volume applications who want fast setup and prepaid usage — not a full observability suite.

The product concentrates on two signals:

- Notifications — production API events delivered to configured browser displays and iPhones.
- Heartbeats — production empty-body check-ins with timeout and recovery alerts.

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

### API base URL

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

### Next steps

Sign in, create a project, copy its reveal-once initial API key, then add a browser display or create a 15-minute code to pair the iOS app.

---

## Quickstart

Send your first browser-display alert in under two minutes.

You'll create a project, open a virtual device, 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. Open a virtual device

Open the project’s Virtual devices tab, create a named display, copy the reveal-once secret URL, and open it in the browser you will leave on screen. Keep the URL private; rotating it immediately replaces the current display session.

### 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.

```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.
```

### 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 iPhone. An eligible iPhone receives independent APNs and foreground SSE attempts; an open stream never suppresses APNs. 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. The iOS app deduplicates by notification ID and stores its 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 iPhone for focused alerts.

A project supports up to ten active devices shared across browser displays and paired iPhones. 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 iPhones.

---

## 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 mobile outcomes. The targets, apnsEligible, apnsSkipped, accepted, failed, and liveTargeted counters separate configured iPhones from APNs provider outcomes and live-stream enqueue attempts; neither acceptance nor enqueue confirms presentation on the iPhone.

> **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
- `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 iPhone pairing-invite metadata without revealing codes
- `POST /api/projects/{projectID}/mobile-invites` — Create a reveal-once iPhone pairing code valid for 15 minutes
- `DELETE /api/projects/{projectID}/mobile-invites/{inviteID}` — Revoke an unused iPhone pairing invite
- `GET /api/projects/{projectID}/mobile-devices` — List paired iPhones
- `DELETE /api/projects/{projectID}/mobile-devices/{deviceID}` — Revoke a paired iPhone
- `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 an iPhone
- `PUT /api/mobile/installations/registration` — Refresh APNs registration for every project paired to one installation
- `GET /api/mobile/installations/events` — Open the installation-wide live iPhone monitor
- `DELETE /api/mobile/devices/{deviceID}` — Unpair the credential holder's iPhone
- `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)
