-
Notifications
You must be signed in to change notification settings - Fork 0
API Reference
Base URL: http://localhost:8080 (or your configured LISTEN_ADDR)
| Scheme | Used by | How to send |
|---|---|---|
| JWT Bearer | All /api/v1/* endpoints and /auth/register
|
Authorization: Bearer <token> |
| App token | POST /message |
X-App-Token: <token> header or ?token=<token> query param |
| WS ticket | GET /ws |
?ticket=<ticket> query param (preferred) |
| None |
/auth/login, /auth/register, /health
|
— |
All error responses share the same JSON shape:
{ "error": "human-readable message" }Common HTTP status codes:
| Code | Meaning |
|---|---|
400 |
Bad request — malformed JSON or invalid field |
401 |
Unauthorized — missing or invalid credentials |
403 |
Forbidden — valid JWT but insufficient permissions |
404 |
Not found |
409 |
Conflict — e.g. username already exists |
429 |
Rate limit exceeded |
500 |
Internal server error |
Liveness check. Pings the database.
Response 200:
{ "status": "ok" }Exchange credentials for a JWT.
Request:
{ "username": "admin", "password": "admin" }Response 200:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_in": 86400
}Errors: 400 invalid body · 401 wrong credentials
Register a new user. Disabled when ALLOW_REGISTRATION=false.
Request:
{ "username": "alice", "password": "s3cur3pass" }Response 201:
{ "id": "1", "username": "alice", "is_admin": false }Errors: 400 invalid body · 409 username taken · 403 registration disabled
All endpoints require Authorization: Bearer <jwt>.
List notifications for the authenticated user.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
integer 1–100 | 20 |
Page size |
offset |
integer | 0 |
Page offset |
app_id |
string | — | Filter by application ID |
read |
true / false
|
— | Filter by read status |
priority |
integer 0–10 | — | Filter by exact priority |
q |
string | — | Full-text LIKE search on title and message |
Response 200:
{
"notifications": [
{
"id": "42",
"title": "Deploy successful",
"message": "v1.2.3 is live on production",
"priority": 7,
"read": false,
"created_at": "2026-03-31T17:00:00Z",
"app": { "id": "3", "name": "CI Pipeline" }
}
],
"total": 142,
"limit": 20,
"offset": 0
}Get a single notification by ID.
Response 200: same object shape as the array element above.
Errors: 404 not found · 403 belongs to another user
Mark a notification as read.
Response 200:
{ "message": "notification marked as read" }Soft-delete a single notification.
Response 200:
{ "message": "notification deleted" }Soft-delete all notifications belonging to the authenticated user.
Response 200:
{ "message": "all notifications deleted" }All endpoints require Authorization: Bearer <jwt>.
List applications owned by the authenticated user.
Response 200:
{
"applications": [
{
"id": "3",
"name": "CI Pipeline",
"description": "GitHub Actions notifications",
"webhook_url": "https://hooks.example.com/notify",
"created_at": "2026-03-01T10:00:00Z"
}
]
}Create a new application. The plaintext app token is returned once — store it securely.
Request:
{
"name": "CI Pipeline",
"description": "GitHub Actions notifications",
"webhook_url": "https://hooks.example.com/notify"
}Response 201:
{
"id": "3",
"name": "CI Pipeline",
"token": "a3f8c2...64 hex chars...e9b1d0"
}Errors: 400 invalid body
Update the name, description, or webhook URL of an application.
Request (all fields optional):
{
"name": "CI Pipeline v2",
"description": "Updated description",
"webhook_url": "https://hooks.example.com/v2"
}Response 200: updated application object (without token)
Delete an application and all its notifications.
Response 200:
{ "message": "application deleted" }Rotate the app token. The old token is immediately invalidated.
Response 200:
{ "token": "new-64-char-hex-token" }Send a notification. Authenticated via app token.
Headers:
X-App-Token: <your-app-token>
Content-Type: application/json
Request:
{
"title": "Deploy successful",
"message": "v1.2.3 is live on production",
"priority": 7
}| Field | Type | Required | Description |
|---|---|---|---|
title |
string | Yes | Notification title |
message |
string | Yes | Notification body |
priority |
integer 1–10 | No | Default 5
|
Response 200:
{
"id": "42",
"title": "Deploy successful",
"message": "v1.2.3 is live on production",
"priority": 7,
"read": false,
"created_at": "2026-03-31T17:00:00Z"
}Errors: 401 invalid or missing app token · 400 invalid body · 429 rate limit
curl example:
curl -s -X POST http://localhost:8080/message \
-H "X-App-Token: your-app-token" \
-H "Content-Type: application/json" \
-d '{"title":"Hello","message":"World","priority":5}' | jqIssue a short-lived WebSocket authentication ticket (requires valid JWT).
Response 200:
{ "ticket": "32-byte-hex-string" }Tickets expire after 30 seconds. Consume immediately.
Open a WebSocket connection. Accepts either a ticket (recommended) or a JWT directly.
GET /ws?ticket=<ticket> ← preferred: no long-lived token in URL
GET /ws?token=<jwt> ← fallback
Incoming message format:
{
"event": "notification",
"notification": {
"id": "42",
"title": "Deploy successful",
"message": "v1.2.3 is live on production",
"priority": 7,
"read": false,
"created_at": "2026-03-31T17:00:00Z",
"app": { "id": "3", "name": "CI Pipeline" }
}
}JavaScript example:
async function connect(jwtToken) {
const { ticket } = await fetch('/api/v1/ws/ticket', {
headers: { Authorization: `Bearer ${jwtToken}` },
}).then(r => r.json());
const ws = new WebSocket(`ws://localhost:8080/ws?ticket=${ticket}`);
ws.onopen = () => console.log('connected');
ws.onmessage = (e) => {
const { event, notification } = JSON.parse(e.data);
if (event === 'notification') {
console.log(`[${notification.priority}] ${notification.title}`);
}
};
ws.onclose = () => console.log('disconnected');
}Connection limits: 54 s ping / 60 s pong timeout. The server closes the connection if a pong is not received within 60 seconds.
All endpoints require Authorization: Bearer <jwt> with is_admin: true.
List all users.
Response 200:
{
"users": [
{ "id": "1", "username": "admin", "is_admin": true, "created_at": "..." },
{ "id": "2", "username": "alice", "is_admin": false, "created_at": "..." }
]
}Create a user.
Request:
{ "username": "bob", "password": "s3cur3", "is_admin": false }Response 201: user object (no password field)
Delete a user and cascade-delete all their applications and notifications.
Response 200:
{ "message": "user deleted" }Reset a user's password.
Request:
{ "password": "newpassword" }Response 200:
{ "message": "password updated" }