API Reference
Base URL: https://api.yourdomain.com
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)
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)
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:
{
"error": "NotFound",
"message": "App not found"
}
| Status | Meaning |
|---|---|
400 | Validation error (bad request body) |
401 | Missing or invalid credentials |
403 | Authenticated but not authorized (wrong org, wrong app scope) |
404 | Resource not found |
409 | Conflict (duplicate) |
422 | Unprocessable entity (semantic error) |
429 | Rate limit exceeded (Retry-After header included) |
500 | Internal 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:
Retry-After: 30
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1716120060
Push
Send to all devices
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
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
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.
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
GET /v1/devices?app_id=uuid&page=1&per_page=50
Authorization: Bearer vp_live_...
Get device
GET /v1/devices/{id}
Update device
PUT /v1/devices/{id}
Content-Type: application/json
{
"external_id": "new-user-id",
"language": "en",
"tags": ["premium", "beta"]
}
Delete device
DELETE /v1/devices/{id}
Opt out / Opt in
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
GET /v1/campaigns?app_id=uuid&page=1&per_page=20
Get campaign
GET /v1/campaigns/{id}
Response includes: id, name, status, audience_size, sent_at, scheduled_at, contents, stats.
Get campaign stats
GET /v1/campaigns/{id}/stats
{
"campaign_id": "uuid",
"sent": 12345,
"delivered": 11800,
"failed": 345,
"bounced": 200,
"throttled": 0
}
Cancel campaign
Cancels a scheduled campaign (only possible before delivery starts).
POST /v1/campaigns/{id}/cancel
Apps
List apps
GET /v1/apps
Create app
POST /v1/apps
Content-Type: application/json
{
"name": "My iOS App",
"platform": "ios",
"bundle_id": "com.example.myapp"
}
Get / Update / Delete app
GET /v1/apps/{app_id}
PUT /v1/apps/{app_id}
DELETE /v1/apps/{app_id}
Platform credentials
GET /v1/apps/{app_id}/platform-config
POST /v1/apps/{app_id}/platform-config
iOS (APNs)
{
"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)
{
"platform": "android",
"fcm_service_account_json": "{\"type\":\"service_account\",...}"
}
Web (VAPID)
{
"platform": "web",
"vapid_public_key": "...",
"vapid_private_key": "...",
"vapid_subject": "mailto:admin@example.com"
}
Credentials are AES-256-GCM encrypted at rest.
API Keys
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:
{ "name": "Production SDK key", "scoped_to_app": true }
Response (key shown only once):
{ "id": "uuid", "key": "vp_live_...", "name": "Production SDK key" }
Analytics
Campaign funnel
GET /v1/analytics/campaigns/{id}/funnel
Returns delivery funnel by platform. Data is queried from Parquet files via DataFusion — no separate OLAP database required.
[
{ "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
GET /v1/analytics/campaigns/{id}/timeline?interval=hour
interval: minute, hour, or day. Returns time-series delivery data for the campaign.
App overview
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
GET /v1/analytics/overview
Returns organization-wide push volume over the last 30 days.
Webhooks
List webhooks
GET /v1/webhooks?app_id=uuid
Create webhook
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
DELETE /v1/webhooks/{id}
Test webhook
Sends a test payload to the webhook URL.
POST /v1/webhooks/{id}/test
Webhook payload
{
"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
GET /v1/organization
List members
GET /v1/organization/members
Invite member
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
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.
{
"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
GET /.well-known/openid-configuration
Authorization endpoint (PKCE)
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
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
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&
refresh_token=<token>&
client_id=admin
User info
GET /oauth/userinfo
Authorization: Bearer <access-token>
Current user
GET /v1/auth/me
Authorization: Bearer <jwt>
Health
Liveness / readiness probe
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
GET /metrics
Returns text/plain in Prometheus exposition format. Scrape interval is configured in the Prometheus / Grafana stack.