Skip to content

API Reference

Base URL: https://api.gesher.pro

Unless noted, JSON bodies use Content-Type: application/json. App routes expect:

http
Authorization: Bearer <APP_API_KEY>

Admin routes expect:

http
Authorization: Bearer <ADMIN_API_KEY>

Field names for app-facing subscribe/unsubscribe bodies are camelCase (userId, not user_id).


Public (no auth)

MethodPathDescription
GET/healthLiveness check
GET/vapid-keyPublic VAPID public key for PushManager.subscribe

GET /health

Response 200

json
{ "ok": true, "ts": "2026-04-05T12:00:00.000Z" }

(ts is an ISO 8601 string.)

GET /vapid-key

Response 200

json
{ "publicKey": "BFM3gqlY5hiALOWCxNL-..." }

Error responses

StatusBody
500{ "error": "VAPID_PUBLIC_KEY is missing or empty in worker bindings" } if the worker is misconfigured

App API (Bearer = app API key)

MethodPathDescription
POST/subscribeRegister or refresh a push subscription
DELETE/unsubscribeRemove one subscription by endpoint
POST/notifyBroadcast to all subscribers (202, queued)
POST/notify/:userIdSend to one user’s devices (200, synchronous)
GET/statsAggregate metrics
GET/stats/subscriptionsPaginated subscribers
GET/stats/deliveriesPaginated delivery aggregates

Common headers

http
Authorization: Bearer <API_KEY>
Content-Type: application/json

POST /subscribe

Registers or updates a subscription for (app_id, userId, endpoint).

Body (JSON)

json
{
  "userId": "string (required)",
  "subscription": {
    "endpoint": "string (required — push endpoint URL)",
    "keys": {
      "p256dh": "string (required)",
      "auth": "string (required)"
    }
  },
  "tags": ["string (optional — stored as JSON array)"]
}

tags defaults to [] if omitted.

Success 200

json
{ "ok": true }

Error responses

StatusWhen
400Missing userId, subscription.endpoint, or keys
401Missing/invalid Bearer token
403App at subscriber cap (max_subscribers) for a new endpoint

403 body (limit)

json
{
  "error": "Subscription limit reached. Upgrade your plan.",
  "limit": 1000,
  "current": 1000
}

DELETE /unsubscribe

Body (JSON)

json
{
  "userId": "string (required)",
  "endpoint": "string (required — push endpoint to remove)"
}

Success 200

json
{ "ok": true }

Error responses

StatusWhen
400Missing userId or endpoint
401Missing/invalid Bearer token

POST /notify (broadcast)

Sends a notification to all subscribers of the authenticated app. Work is continued in the background; the response is immediate.

Body (JSON)

json
{
  "title": "string (required)",
  "body": "string (optional)",
  "url": "string (optional — opened on notification click)",
  "icon": "string (optional)",
  "badge": "string (optional)",
  "tag": "string (optional — replaces same tag)"
}

Success 202

json
{
  "ok": true,
  "notificationId": "550e8400-e29b-41d4-a716-446655440000",
  "status": "queued"
}

Error responses

StatusWhen
400Missing title
401Missing/invalid Bearer token
429Notify rate limit exceeded for the app’s plan

429 body

json
{
  "error": "Rate limit exceeded",
  "limit": 100,
  "window": "1 minute"
}

limit depends on plan: free 100, starter 200, pro 10000 per rolling minute; enterprise is not limited in code.


POST /notify/:userId (targeted)

Same JSON body as broadcast.

Success 200

When the user has subscriptions:

json
{
  "ok": true,
  "sent": 2,
  "errors": 0,
  "notificationId": "550e8400-e29b-41d4-a716-446655440000"
}

When none:

json
{
  "ok": true,
  "sent": 0,
  "errors": 0,
  "note": "No subscriptions found"
}

(notificationId is omitted in the “no subscriptions” case.)

Error responses

StatusWhen
400Missing title
401Missing/invalid Bearer token
429Same as broadcast

GET /stats

Success 200

json
{
  "subscribers": 150,
  "sent_24h": 42,
  "failed_24h": 3
}
  • failed_24h counts delivery log rows in the last 24h with status failed or gone.

Error responses

StatusWhen
401Missing/invalid Bearer token

GET /stats/subscriptions

Cursor-based pagination (50 rows per page). Cursor is the user_id of the last row from the previous page (string compare / ordering as implemented in the worker).

Query

ParamDescription
cursorOptional; omit for first page

Success 200

json
{
  "data": [
    {
      "user_id": "user-123",
      "device_count": 2,
      "last_active": "2026-04-05 10:00:00"
    }
  ],
  "next_cursor": "user-xyz"
}

next_cursor is null when there is no next page.


GET /stats/deliveries

Paginated aggregates per notification_id (50 per page). Cursor is sent_at of the last row from the previous page.

Query

ParamDescription
cursorOptional; omit for first page

Success 200

json
{
  "data": [
    {
      "notification_id": "550e8400-e29b-41d4-a716-446655440000",
      "sent": 10,
      "failed": 1,
      "gone": 2,
      "sent_at": "2026-04-05 12:00:00"
    }
  ],
  "next_cursor": "2026-04-05 11:00:00"
}

Admin API (Bearer = admin API key)

MethodPathDescription
POST/admin/appsCreate app
GET/admin/appsList apps (keys redacted)
GET/admin/apps/:idGet one app (includes api_key)
PATCH/admin/apps/:idPartial update
DELETE/admin/apps/:idDelete app

POST /admin/apps

Body (JSON) — create

FieldTypeRequiredNotes
idstringYesLowercase letters, numbers, hyphens only
namestringYesDisplay name
api_keystringYesMin 32 characters; must be unique
owner_emailstringNo
domainstringNo
planstringNo'free' | 'starter' | 'pro' | 'enterprise'; default free
max_subscribersnumberNoPositive integer; default 1000
webhook_urlstringNo

Success 201

json
{ "ok": true, "id": "my-app", "name": "My App" }

Error responses

StatusWhen
400Validation (missing fields, bad id, short api_key, invalid plan / max_subscribers)
401Invalid admin token
409Duplicate id or api_key

GET /admin/apps

Success 200

json
{
  "apps": [
    {
      "id": "my-app",
      "name": "My App",
      "owner_email": null,
      "domain": null,
      "plan": "free",
      "max_subscribers": 1000,
      "is_active": 1,
      "created_at": "..."
    }
  ]
}

(API keys are not included in list.)


GET /admin/apps/:id

Success 200 — full row including api_key.

Error responses

StatusWhen
401Invalid admin token
404Unknown id

PATCH /admin/apps/:id

Partial update. Only include fields to change.

Allowed fields

FieldTypeNotes
namestringRequired if present (non-null)
owner_emailstring | null
domainstring | null
webhook_urlstring | null
planstringOne of the plan enum values
max_subscribersnumberPositive integer
is_activeboolean or 0 / 1

Success 200

json
{ "ok": true, "id": "my-app" }

Error responses

StatusWhen
400Invalid field values or empty patch
401Invalid admin token
404Unknown id

DELETE /admin/apps/:id

Success 200

json
{ "ok": true }

Error responses

StatusWhen
401Invalid admin token
404Unknown id

Error model

Most errors return JSON with an error string. Typical status codes:

CodeUsage
400Validation / malformed JSON
401Missing or wrong Bearer token
403Subscriber limit reached on subscribe
404Admin app not found
409Duplicate app on create
429Notify rate limit
500Server / configuration error (e.g. VAPID binding missing)

There is no dedicated 502 JSON contract from Gesher today; push delivery failures are recorded in delivery_log (see stats) rather than returned on the HTTP response for broadcast.

Gesher — managed web push by Otomator