meduzavpn
EN
Candidate contract · first release scope

Meduza Business API

Manage companies, members, VPN access and routes through the same API the company workspace uses.

This is the contract of the current business branch. A ready handler does not mean it is published to production: external payments, VPN provisioning, email and policy delivery work when the environment adapters are connected.

First release: 169 methods of the current contract — not 169 of 191 completed requirements. The draft uses different paths and flows. Full first release matrix: SSO/SCIM, ownership transfer, router firmware deployment, configuration export and legal invoice details are excluded. notification-channels are a legacy inventory only: their contract and email/webhook/in_app delivery are excluded from the first release. Seven alert types are available in monitoring, audit and the API; delivery through the separate signed webhooks API remains in scope. Routers and resources are inventory for now; billing-profile only stores metadata and is not linked to invoices.

Authorization and permissions

Base path: https://api.meduzavpn.com/api/business/v1. The interface uses the current user session. For automation, create an API client in the company workspace and save the secret when it is issued.

curl -X POST https://api.meduzavpn.com/api/business/v1/oauth/token \
  -d grant_type=client_credentials \
  -d client_id=YOUR_CLIENT_ID \
  -d client_secret=YOUR_SECRET

curl https://api.meduzavpn.com/api/business/v1/organizations/ORGANIZATION_ID/members \
  -H "Authorization: Bearer TOKEN"

Pass the token you receive in Authorization: Bearer TOKEN. The token is valid for one hour and only within the company that issued the client. Access is revoked when the client is disabled or when the administrator who created it loses the rights.

Permissions

read — read only; read write — read and manage. API clients are managed by a signed-in user. The billing role sees invoices and the subscription, but not members or networks.

Retries

Use Idempotency-Key for changes. When editing, send If-Match: "3" with the object's current version. On a 412 conflict, read the object again.

Billing and VPN provisioning

  1. Create a company with POST /organizations from a user session.
  2. Get a quote via subscription/quotes: number of VPNs, period and the purchase, renewal or upgrade operation.
  3. Pass quote_id and payment_method: stripe_card to subscription/checkout.
  4. A 202 response means the invoice is being prepared. Poll invoices until payment_url appears; pay using that link.
  5. Once the payment ledger confirms the payment, the subscription becomes active. Create VPNs with a pricing_id from catalog/vpn-locations and a mandatory Idempotency-Key.

A package starts at 5 VPNs. Base price per VPN: $5.99 a month or $54.99 a year. Package discount: 5–9 VPNs — 5%, 10–99 — 10%, 100 and more — 20%. Returning to the browser after payment does not grant access until the payment is verified.

A renewal keeps the quantity and period. Enlarging the package charges the difference for the remaining term. To remove a server permanently, use POST /organizations/{organization_id}/vpns/{id}/retire with confirm: true and If-Match. The slot is freed after the removal is confirmed; the subscription price does not change. Assignment to an employee is passed as member_id; without it, the VPN is available to the company according to its access assignments.

Who can use a company VPN is set by the access field of PATCH /organizations/{organization_id}/vpns/{id} with If-Match. company opens it to every company member whose role allows connecting; the member_ids and group_ids lists are cleared. restricted limits it to the listed employees and members of the listed groups; empty lists remove everyone’s access. company together with non-empty lists is rejected with 422. GET responses and /me/vpn-grants always include data.access, including for VPNs created before the field existed. A request without access but with member_ids or group_ids switches the VPN to restricted, as before. A dedicated employee VPN is always restricted. Mode changes are recorded in the audit as vpn_access_changed; when access is narrowed, credentials of devices that lost access are revoked.

curl -X PATCH https://api.meduzavpn.com/api/business/v1/organizations/ORGANIZATION_ID/vpns/VPN_ID \
  -H "Authorization: Bearer TOKEN" \
  -H 'If-Match: "3"' \
  -H "Idempotency-Key: vpn-access-1" \
  -H "Content-Type: application/json" \
  -d '{"access":"company"}'

Routes and traffic sets

A set describes websites or networks; a rule picks the action: a specific VPN, direct, or block. Use countries and regions from catalog/traffic-presets. In first-match mode rules are checked top to bottom, up to 128; blocking is not available for “All other traffic”.

Saving creates a draft. Run validation, then publish with If-Match. Check policy-deployments: being queued and being confirmed as applied by a device are different states. The client app is what connects to the VPN.

Roles, groups and devices

GET /organizations/{id}/roles returns built-in and custom roles, permission definitions and the can_manage flag. The owner creates a custom role based on the administrator, member or billing role, keeping only the permissions needed. Assign it via PATCH members/{id}: role and custom_role_id. Permission changes also apply to the API clients created by that user.

A role allows actions in the workspace. group_ids gives an employee VPNs and a routing package; group membership grants no administrative rights. The owner role cannot be copied into a custom one. A role assigned to employees cannot be deleted.

Devices get a display_name, an owner and VPN details. PATCH devices/{id} changes only the name, checked with If-Match. POST devices/{id}/revocations requests revocation; the final status is confirmed by the node. Your own devices are available via /me/devices. The last_api_ip field is the API transport address, not the VPN connection. The name and model reported by the app are not used for authorization.

Connections and destination log

GET /organizations/{id}/employee-analytics shows observed connections, state changes and hourly history for 7 days. A missing measurement is not replaced with zero; the number of sessions is not an employee's working time.

GET /organizations/{id}/activity returns connection requests recorded by the node: the requested domain or IP, port, protocol, time, decision, device and employee. Filters: from, to, member_id, action, search; pagination: cursor and limit. The domain is the one the client sent in the connection request, not something obtained by decrypting HTTPS.

Collection is off by default. The owner changes activity-settings via PATCH with {"enabled": true} and If-Match. Reading requires activity.read. When collection is on, the app shows the employee a notice; issuing a key requires an up-to-date X-Corporate-Activity-Version acknowledgement after the key owner is verified. An old client does not get a key for this mode.

Retention is limited to 7 days and 100,000 events per company; the node buffer holds up to 24 hours. coverage reports freshness, unreachable devices and buffer losses. Personal VPNs, direct device traffic, local blocks before reaching the node, HTTPS paths, request contents and productivity scoring are not recorded. Turning collection off does not restore missed history and does not instantly delete records already collected: the retention period applies.

Categories and network activity time

The log response contains analysis: distribution by category, employee and hour, top destinations and the number of active_minutes. Summaries are calculated over all stored events for the selected period, regardless of the page of the raw log. The category filter takes you to specific events.

One active minute is a unique minute of an employee in which the node observed a request. It is not the time spent viewing a website or working: background traffic counts, and categories can overlap within one minute. The total time is calculated separately and is not the sum of the categories.

The owner sets up to 64 classification_rules in the logging settings: a domain and a category. They apply to subdomains and take priority over the built-in directory. Unknown destinations stay in a separate category. The current rules classify the whole selected period; the response states the classification version.

What is processed are destinations of company connections, not HTTPS content, full URLs or personal traffic. The notice acknowledgement is stored once per employee in a company; changing categories, groups or the collection mode does not require acknowledging the same notice again. Incomplete coverage flags and truncated must not be read as absence of activity.

Event subscriptions

Create webhooks with a public HTTPS URL on port 443 and the events audit.event, * or groups.created, members.updated and other resource changes. The secret is shown only on creation or rotation.

Meduza-Signature: t=UNIX_SECONDS,v1=HEX_SIGNATURE
Meduza-Event-Id: EVENT_ID
Meduza-Delivery-Id: DELIVERY_ID

HEX_SIGNATURE = HMAC-SHA256(secret, timestamp + "." + RAW_JSON_BODY)

Verify the signature against the raw body bytes, the allowed timestamp age and repeated event IDs. During rotation, keep the previous secret for events already queued. Any 2xx counts as success; other responses are retried with increasing intervals, up to eight attempts. History and manual redelivery are available via deliveries. Successful and cancelled deliveries are kept for up to 30 days, at most 2,000 per webhook. The queue and failures are limited to 1,000 events; on overflow the oldest events are dropped and the dropped_deliveries counter reports the loss. In that case, resync the state through the API.

Delivery is at least once. Besides changes made through the API, confirmed changes to invoices (invoices.updated), subscriptions (subscriptions.updated), VPN readiness (vpns.updated) and alerts (alerts.created) are sent. Polling again without a state change does not create a new event. This is not a network traffic stream. Private IPs, redirects and calls to internal services are forbidden.

API methods

Loading the contract…

List pages and filters

For inventory lists, pass limit=1..200, then cursor from next_cursor, keeping the other parameters. Without limit/cursor, older clients get the full list: data is never silently truncated. Available filters: status, member_id, user_id, vpn_id, search and created_from/created_to (RFC3339, lower bound inclusive, upper bound exclusive). Access is checked before filtering and on every page. Unknown or repeated parameters return 422. Activity log lists use a separate contract. Parameters of each method.

What this version covers

The administrative audit is kept for up to 90 days, at most the latest 10,000 events per company; expired idempotency records are cleaned up. This cleanup does not delete invoices or subscriptions. Inventory lists return items, total and next_cursor; a continuation appears with an explicit limit/cursor. Without them, the full response stays compatible with older clients. The activity log separately supports a cursor and the documented filters. Secrets are not repeated in responses served from the idempotency cache. Confirmed key revocation comes from the VPN server; disabling a membership immediately blocks new issuance.

Monitoring shows actual connections and load. Destination history is available through a separate log that must be explicitly enabled. HTTPS content and employee productivity scoring are not part of this data. SSO/SCIM and other methods from the extended draft are not part of this implemented contract.

Management errors return application/problem+json with status, detail and request_id. OAuth token errors use the error field.