GitHub Dashboard →

API Reference

Base URL: https://api.yourdomain.com

Interactive docs: Swagger UI is available at GET /docs. The raw OpenAPI 3.1 spec is at GET /openapi.json. Both are served by the running API process and always reflect the current version.

Authentication

All requests (except /healthz and /metrics) require an Authorization header.

API Key (SDK / server-to-server)

http
Authorization: Bearer vp_live_<key>

API keys are created in the admin panel under Apps → API Keys. A key can be scoped to a specific app (restricts cross-app operations) or organization-wide.

Session JWT (admin panel)

http
Authorization: Bearer <jwt>

JWTs are issued by the OAuth 2.1 token endpoint after completing the PKCE flow. See the OAuth / OIDC section below.

Error Format

All errors return JSON:

json
{
  "error": "NotFound",
  "message": "App not found"
}
Status Meaning
400Validation error (bad request body)
401Missing or invalid credentials
403Authenticated but not authorized (wrong org, wrong app scope)
404Resource not found
409Conflict (duplicate)
422Unprocessable entity (semantic error)
429Rate limit exceeded (Retry-After header included)
500Internal server error

Rate Limiting

Requests are rate-limited per API key: 1,000 requests/minute by default. When exceeded, the API returns 429 Too Many Requests with the following headers:

http
Retry-After: 30
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1716120060

Push

Send to all devices

http
POST /v1/push/send
Authorization: Bearer vp_live_...
Content-Type: application/json

{
  "app_id": "uuid",
  "name": "My campaign",
  "contents": {
    "en": { "title": "Hello!", "body": "This is a push notification." },
    "de": { "title": "Hallo!", "body": "Das ist eine Benachrichtigung." }
  },
  "data": { "screen": "home" },
  "scheduled_at": "2026-06-01T09:00:00Z"
}

contents is a map of language code → title/body. Devices receive the content matching their language, falling back to "en".

scheduled_at is optional — omit for immediate delivery.

Send to specific users

http
POST /v1/push/send-to-users
Authorization: Bearer vp_live_...
Content-Type: application/json

{
  "app_id": "uuid",
  "external_ids": ["user-123", "user-456"],
  "name": "Targeted push",
  "contents": {
    "en": { "title": "Hi there!", "body": "Just for you." }
  }
}

Send to segment

http
POST /v1/push/send-to-segment
Authorization: Bearer vp_live_...
Content-Type: application/json

{
  "app_id": "uuid",
  "segment_id": "uuid",
  "name": "Segment push",
  "contents": {
    "en": { "title": "Segment message", "body": "..." }
  }
}

Devices

Register a device

Called from the mobile SDK when the app starts or the push token changes. Upserts by push token.

http
POST /v1/devices/register
Authorization: Bearer vp_live_...
Content-Type: application/json

{
  "app_id": "uuid",
  "push_token": "<fcm-or-apns-token>",
  "platform": "android",
  "external_id": "user-123",
  "language": "de",
  "timezone": "Europe/Berlin",
  "app_version": "2.1.0"
}

platform: "ios", "android", or "web". Response: 200 OK (upserts by push token).

List devices

http
GET /v1/devices?app_id=uuid&page=1&per_page=50
Authorization: Bearer vp_live_...

Get device

http
GET /v1/devices/{id}

Update device

http
PUT /v1/devices/{id}
Content-Type: application/json

{
  "external_id": "new-user-id",
  "language": "en",
  "tags": ["premium", "beta"]
}

Delete device

http
DELETE /v1/devices/{id}

Opt out / Opt in

http
POST /v1/devices/{id}/opt-out
POST /v1/devices/{id}/opt-in

Opt-out suppresses all future push notifications for the device without deleting the registration.

Campaigns

Campaigns are created by the push endpoints above. These endpoints let you inspect and manage them.

List campaigns

http
GET /v1/campaigns?app_id=uuid&page=1&per_page=20

Get campaign

http
GET /v1/campaigns/{id}

Response includes: id, name, status, audience_size, sent_at, scheduled_at, contents, stats.

Get campaign stats

http
GET /v1/campaigns/{id}/stats
json — response
{
  "campaign_id": "uuid",
  "sent": 12345,
  "delivered": 11800,
  "failed": 345,
  "bounced": 200,
  "throttled": 0
}

Cancel campaign

Cancels a scheduled campaign (only possible before delivery starts).

http
POST /v1/campaigns/{id}/cancel

Apps

List apps

http
GET /v1/apps

Create app

http
POST /v1/apps
Content-Type: application/json

{
  "name": "My iOS App",
  "platform": "ios",
  "bundle_id": "com.example.myapp"
}

Get / Update / Delete app

http
GET    /v1/apps/{app_id}
PUT    /v1/apps/{app_id}
DELETE /v1/apps/{app_id}

Platform credentials

http
GET  /v1/apps/{app_id}/platform-config
POST /v1/apps/{app_id}/platform-config

iOS (APNs)

json
{
  "platform": "ios",
  "apns_p8_key": "-----BEGIN PRIVATE KEY-----\n...",
  "apns_key_id": "ABCD1234EF",
  "apns_team_id": "TEAM12345",
  "apns_bundle_id": "com.example.myapp",
  "apns_environment": "production"
}

Android (FCM v1)

json
{
  "platform": "android",
  "fcm_service_account_json": "{\"type\":\"service_account\",...}"
}

Web (VAPID)

json
{
  "platform": "web",
  "vapid_public_key": "...",
  "vapid_private_key": "...",
  "vapid_subject": "mailto:admin@example.com"
}

Credentials are AES-256-GCM encrypted at rest.

API Keys

http
GET    /v1/apps/{app_id}/api-keys
POST   /v1/apps/{app_id}/api-keys
DELETE /v1/apps/{app_id}/api-keys/{key_id}

Create request body:

json
{ "name": "Production SDK key", "scoped_to_app": true }

Response (key shown only once):

json — response
{ "id": "uuid", "key": "vp_live_...", "name": "Production SDK key" }

Analytics

Campaign funnel

http
GET /v1/analytics/campaigns/{id}/funnel

Returns delivery funnel by platform. Data is queried from Parquet files via DataFusion — no separate OLAP database required.

json — response
[
  { "event_type": "sent",      "platform": "android", "count": 10000 },
  { "event_type": "delivered", "platform": "android", "count":  9500 },
  { "event_type": "failed",    "platform": "android", "count":   300 },
  { "event_type": "bounced",   "platform": "android", "count":   200 }
]

Campaign timeline

http
GET /v1/analytics/campaigns/{id}/timeline?interval=hour

interval: minute, hour, or day. Returns time-series delivery data for the campaign.

App overview

http
GET /v1/analytics/apps/{id}/overview?from=2026-05-01&to=2026-05-19

Returns daily aggregated stats for an app over the date range.

Organization overview

http
GET /v1/analytics/overview

Returns organization-wide push volume over the last 30 days.

Webhooks

List webhooks

http
GET /v1/webhooks?app_id=uuid

Create webhook

http
POST /v1/webhooks
Content-Type: application/json

{
  "app_id": "uuid",
  "url": "https://your-server.com/webhook",
  "events": ["delivered", "failed", "bounced"],
  "secret": "optional-hmac-secret"
}

Available events: sent, delivered, failed, bounced, throttled, expired

Delete webhook

http
DELETE /v1/webhooks/{id}

Test webhook

Sends a test payload to the webhook URL.

http
POST /v1/webhooks/{id}/test

Webhook payload

json
{
  "event": "delivered",
  "campaign_id": "uuid",
  "device_id": "uuid",
  "external_id": "user-123",
  "platform": "android",
  "timestamp": "2026-05-19T12:00:00Z"
}

If a secret was configured, the payload is HMAC-SHA256 signed. The signature is in the X-VisionPush-Signature header.

Organization

Get organization

http
GET /v1/organization

List members

http
GET /v1/organization/members

Invite member

http
POST /v1/organization/members
Content-Type: application/json

{
  "email": "newuser@example.com",
  "role": "developer"
}

An invite email is sent via SMTP. Roles: viewer, developer, admin, owner.

Media

Upload image

http
POST /v1/media/upload
Authorization: Bearer vp_live_...
Content-Type: multipart/form-data

file=@image.jpg

Accepted types: image/jpeg, image/png, image/gif, image/webp. Max size: 10 MiB.

json — response
{
  "url": "https://cdn.example.com/media/ab/cd/<blake3-hash>.jpg",
  "content_type": "image/jpeg",
  "size": 124800
}

Images are content-addressed (BLAKE3 hash) — identical files are deduplicated automatically.

OAuth / OIDC

OIDC discovery

http
GET /.well-known/openid-configuration

Authorization endpoint (PKCE)

http
GET /oauth/authorize?
  response_type=code&
  client_id=admin&
  redirect_uri=https://app.yourdomain.com/callback&
  code_challenge=<S256-challenge>&
  code_challenge_method=S256&
  scope=openid profile email

Token exchange

http
POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=<auth-code>&
redirect_uri=https://app.yourdomain.com/callback&
code_verifier=<pkce-verifier>&
client_id=admin

Refresh token

http
POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&
refresh_token=<token>&
client_id=admin

User info

http
GET /oauth/userinfo
Authorization: Bearer <access-token>

Current user

http
GET /v1/auth/me
Authorization: Bearer <jwt>

Health

Liveness / readiness probe

http
GET /healthz

Returns 200 OK with {"status":"ok"} when the API and database connection are healthy. Used by Kubernetes liveness and readiness probes.

Prometheus metrics

http
GET /metrics

Returns text/plain in Prometheus exposition format. Scrape interval is configured in the Prometheus / Grafana stack.