Platform reference
HeyValue Provisioning API
The provisioning API creates and configures your customers and the products they use, through one API-first contract. It covers accounts, companies, licensing and entitlements, and the Shield DNS-security product for the HeyValue tenant.
How the pieces fit
An Envoy API gateway fronts every product service on one /v1/* surface. Each request passes a three-stage, fail-closed policy chain before it reaches a service:
Client ──▶ Gateway ──▶ [1] Authenticate (JWT) ──▶ [2] Authorize (OPA) ──▶ [3] License (entitlement) ──▶ Service
api.heyvalue.com realm pyme-heyvalue ownership / role plan / quota / usage accounts · licensing · shieldThe gateway verifies the caller's token, injects tenant identity as headers, and only then routes to accounts-api, licensing-api, or shield-api.
Base URL & environments
All paths are relative to the tenant's gateway host. For HeyValue:
| Environment | Base URL | Identity realm |
|---|---|---|
| Production (HeyValue) | https://api.heyvalue.com | pyme-heyvalue |
| Development clone | https://api.digital-smb.dev | pyme-digitalsmb |
Shield endpoints are exposed under /v1/shield/… at the gateway and rewritten to /v1/… on the upstream Shield service. The paths here are the gateway-facing ones. Service-to-service callers that reach shield-api directly use the un-prefixed /v1/… form.
Authentication
The platform uses OpenID Connect (Keycloak). Obtain a token from the HeyValue realm and send it as a bearer token; the gateway validates it, runs authorization and licensing, then injects verified tenant identity to the upstream service.
curl https://api.heyvalue.com/v1/shield/service \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "X-TARGET-COMPANY-ID: 4c1e…company-uuid"Three authentication planes
| Plane | How the caller authenticates | Tenant context |
|---|---|---|
| End-user / integrator | Authorization: Bearer <JWT> from the HeyValue realm | Gateway derives it from the token and injects x-tenant-id + x-target-company-id |
| Service-to-service (M2M) | Client-credentials JWT (M2M audience) | Sends X-TENANT-ID / X-TARGET-COMPANY-ID directly |
| Public device endpoints | None — device link/token/DoH are unauthenticated | Tenant passed as ?tid=pymeheyvalue query |
Downstream services trust X-TENANT-ID and X-TARGET-COMPANY-ID and perform no token validation of their own. When calling through the gateway you do not set these yourself — the gateway sets them from your verified JWT. They are only sent manually on the internal service-to-service plane.
Conventions
Versioning. Path-based; every endpoint lives under /v1. Content type. Requests and responses are JSON (application/json).
Response envelope
Shield service responses are wrapped in a fixed envelope. data carries the payload, errors is null on success, and status mirrors the HTTP status code.
{
"data": { "…": "…" },
"errors": null,
"status": 200
}Many mutating Shield endpoints return a bare boolean payload on success:
{ "data": true, "errors": null, "status": 200 }Identity headers
| Header | Meaning | Set by |
|---|---|---|
X-TENANT-ID | Tenant / operator id (e.g. pymeheyvalue) | Gateway (from JWT) or M2M caller |
X-TARGET-COMPANY-ID | The customer company being acted on | Gateway (from JWT) or M2M caller |
X-SUBJECT-ID | Acting user id — platform gateway header only | Gateway (not read by shield-api) |
Errors & status codes
Errors use the same envelope with a populated errors array. Each entry has exactly three fields — message, field, ref. There is no code or details field; the machine-readable status is the numeric status.
{
"data": null,
"errors": [
{ "message": "non zero value required", "field": "company_id", "ref": "" }
],
"status": 400
}Across the Shield service subtree the only status codes emitted are 200, 201 (device/rule/simulation create), 204 (empty simulation lookups), 400 (bind/validation), and 500. Missing resources — unknown company profile, unknown plan — surface as 500, not 404. Handle absence by checking the error message, not the status alone.
| Status | Meaning |
|---|---|
200 | Success |
201 | Resource created (device, rule, simulation) |
204 | No content (no active/next simulation; authorize callback ack) |
400 | Request bind / validation failure |
403 | Rejected by the licensing / authorization check at the gateway |
500 | Service error — includes "not found" and "plan not found" in Shield |
How provisioning works
Provisioning is event-driven. Creating a company automatically materializes its licensing entitlements; enabling a product then configures the product service. The gateway coordinates a two-phase commit so metered entitlements (like device seats) are counted exactly once.
- Create the company
POST /v1/companies. The gateway's licensing check reserves any consumed entitlement and flags a post-response callback. - Auto-provision entitlementsOn
201, the gateway calls licensing back; licensing runsresetCompany(company_id, tenant_id)— writing the company's per-service config and seeding limits from the tenant defaults. - Enable a productAn admin
PUTs the company's Shield service config. Licensing then calls Shield:POST /v1/service {company_id, enabled, plan}to provision the subscription. - Provision devicesCreate devices (or short-code links) under the company, then deliver a DoH or WireGuard profile to each device.
- Commit the reservationThe gateway posts the upstream result to
POST /v1/authorize/response; licensing confirms the reservation on success or rolls it back on failure.
Both the OPA and licensing checks run with failure_mode_allow: false. If either is unreachable or denies, the request is rejected. A licensing rejection returns 403 with header x-authz-licensing-status: rejected.
Accounts & Companies
Handled by accounts-api. Companies model the tenant's customers as a hierarchy; accounts are end users, each mirrored into the tenant's Keycloak realm on creation.
/v1/companiesaccounts-apiCreate a customer company. A 201 here is what triggers automatic licensing provisioning (see How provisioning works).
| Field | Type | Req | Notes |
|---|---|---|---|
tenant_id | string | required | e.g. pymeheyvalue |
reference_id | string | required | External correlation id |
name | string | optional | 3–255 chars; defaults to reference_id |
owner_id | uuid | optional | Added as an owner membership |
parent_company_id | uuid | optional | Parent in the org tree |
email, phone, tax_id, address | … | optional | Contact / billing detail |
curl -X POST https://api.heyvalue.com/v1/companies \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tenant_id": "pymeheyvalue",
"reference_id": "acme-001",
"name": "Acme Ltd"
}'{
"id": "4c1e…",
"tenant_id": "pymeheyvalue",
"reference_id": "acme-001",
"name": "Acme Ltd",
"membership": [],
"path": "…",
"created_at": "2026-07-08T10:00:00Z"
}/v1/accountsaccounts-apiCreate an end-user account. The service provisions a matching user in the tenant's Keycloak realm (Auth0 fallback where configured) and stores the external id.
| Field | Type | Req | Notes |
|---|---|---|---|
tenant_id | string | required | Selects the realm/client |
email | required | Login identity | |
password | string | optional | Auto-generated (16 chars) if omitted |
first_name, last_name, phone, locale | … | optional | phone is E.164 |
tenant_memberships | string[] | optional | Extra tenant memberships |
Licensing & entitlements
Handled by licensing-api under /v1/licensing. It stores the tenant's service catalog and each company's effective entitlements, and acts as the gateway's real-time entitlement check.
Service configuration
| Method | Path | Purpose |
|---|---|---|
| GET | /v1/licensing/services/config | Default service catalog |
| GET PUT | /v1/licensing/tenants/{tid}/services/config | Per-tenant defaults |
| GET | /v1/licensing/companies?tid={tid} | Licensing overview for all companies (paged) |
| GET | /v1/licensing/companies/{cid} | One company's full licensing overview |
| GET PUT | /v1/licensing/companies/{cid}/services/{sname}/config | Per-company, per-service config |
| GET | /v1/licensing/companies/{cid}/services/usage | Limit values + current usage |
| POST | /v1/licensing/tenants/{tid}/companies/{cid}/services/reset | (Re)provision a company from tenant defaults |
Enabling Shield for a company is a PUT to its shield service config. The Shield service config carries the plan catalog, the device-seat limit, and the safe-search feature:
{
"name": "shield",
"available": true,
"enabled": true,
"limits": { "shield_devices_per_company": { "value": 5, "usage": 2 } },
"features": { "shield_safe_search": { "enabled": true } },
"plans": {
"available": ["basic","family_parental","business_productivity","faithguard","parental","school"],
"current": "family_parental"
}
}On PUT, licensing calls Shield POST /v1/service with {company_id, enabled, plan} and the X-TENANT-ID header — provisioning the subscription described in Configure / enroll.
Shield · Service lifecycle
The /v1/shield/service endpoints enroll, plan-switch, suspend, and remove a company's Shield subscription. GET/filtering/internet-access read the company from the X-TARGET-COMPANY-ID header; POST/DELETE take company_id in the body.
/v1/shield/serviceReturn the full provisioning status for the company in X-TARGET-COMPANY-ID.
{
"company_id": "4c1e…",
"tenant_id": "pymeheyvalue",
"plan": "family_parental",
"enabled": true,
"filtering_active": true,
"suspended_until": null,
"internet_access_filtering_active": true,
"internet_access_suspended_until": null
}enabled = not disabled. filtering_active = enabled and not suspended. The *_until fields are non-null only for a time-bounded suspension.
/v1/shield/serviceEnroll a company or update its subscription — set/change the plan and/or enable/disable the service. A first-time enrollment requires X-TENANT-ID.
| Field | Type | Req | Notes |
|---|---|---|---|
company_id | string | required | Target company |
plan | string | optional | A plan name; omit to leave unchanged |
enabled | bool | optional | Omit to leave unchanged |
curl -X POST https://api.heyvalue.com/v1/shield/service \
-H "Authorization: Bearer $SERVICE_TOKEN" \
-H "X-TENANT-ID: pymeheyvalue" \
-H "Content-Type: application/json" \
-d '{ "company_id": "4c1e…", "enabled": true, "plan": "family_parental" }'Changing the plan runs in a single transaction that wipes and rebuilds every device's categories, applications, and app-sourced rules from the new plan's defaults, then emits per-device policy events to the resolver fleet. Enable/disable toggles the subscription without touching device policy.
/v1/shield/serviceCompletely remove a company from Shield. Body: { "company_id": "…" }. Deletes all applications, categories, rules, IP mappings, devices, and links; releases WireGuard peers back to the pool; deletes the profile; and emits teardown events. Returns { "data": true }.
Filtering — suspend / resume
/v1/shield/service/filteringTrimmed, console-facing view: { "enabled": <filtering_active>, "plan": "…", "suspended_until": "…"? }.
/v1/shield/service/filteringSuspend or resume filtering for the company (header X-TARGET-COMPANY-ID).
| Field | Type | Notes |
|---|---|---|
enabled | bool | true resumes, false suspends |
duration | string | Optional Go duration (e.g. "2h") — auto-resume window when suspending |
Internet access
/v1/shield/service/internet_access/v1/shield/service/internet_accessSuspend or restore internet access for the whole company (profile and every device), optionally time-bounded. Body is { enabled, duration? }. Validation: duration may not be set when enabling; when disabling it must be positive.
Shield · Device provisioning
Devices live under a company. Create a device directly (server-generated identity) or issue a short-code link that a device redeems at setup. Both paths converge on delivering a filtering profile (DoH or WireGuard) and, for managed clients, a device auth token.
/v1/shield/devicesCreate and enroll a device. The device is allocated a WireGuard peer; doh/dot devices also receive a short code. Requires X-TENANT-ID and X-TARGET-COMPANY-ID.
| Field | Type | Req | Notes |
|---|---|---|---|
name | string | required | 3–255 chars |
type | enum | required | ios · android · windows · macos |
connection_method | enum | optional | doh · dot · vpn |
managed | bool | optional | Managed client flag |
{
"id": "dev-uuid",
"tenant_id": "pymeheyvalue",
"company_id": "4c1e…",
"name": "Kids iPad",
"type": "ios",
"connection_method": "doh",
"short_code": "K7Q2ABX",
"dns_provisioning_endpoint": "https://K7Q2ABX.dns.heyvalue.com/dns-query",
"dns_server_addrs": ["…"],
"created_at": "2026-07-08T10:05:00Z"
}List, get, update, delete
| Method | Path | Notes |
|---|---|---|
| GET | /v1/shield/devices?include=heartbeat | List company devices; optional heartbeat signals |
| GET | /v1/shield/devices/{deviceId} | One device, with ClickHouse-backed heartbeat |
| POST | /v1/shield/devices/{deviceId} | Rename ({ "name": "…" }) |
| DELETE | /v1/shield/devices/{deviceId} | Delete device |
| POST | /v1/shield/devices/{deviceId}/heartbeat/reset | Reset heartbeat |
Enrollment — short-code links
A link lets an end user enroll a device without pre-shared credentials: create the link (get a 6-digit code), the device looks it up, then submits its public key to bind itself.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /v1/shield/devices/link | JWT | Create a link — returns a 6-digit code |
| GET | /v1/shield/devices/link/{code}?tid=pymeheyvalue | Public | Look up a link by code |
| POST | /v1/shield/devices/link/{code}?tid=pymeheyvalue | Public | Enroll — submit device public key |
Create-link body: name (3–255), type (ios·android·windows·macos), optional connection_method, optional device_id (reuse to restart setup). Enroll body requires public_key_der or public_key_pem — an ECDSA P-256 SPKI key, plus optional meta.
/v1/shield/devices/tokenpublicExchange a signed challenge for a device JWT (KMS-signed, ES256). The device signs deviceID + "." + timestamp with its P-256 key; the timestamp must be within ±5 minutes.
{
"device_id": "dev-uuid",
"timestamp": 1751971200,
"signature": "base64url(R||S)"
}{ "token": "eyJhbGciOiJFUzI1Ni…" }Verification keys are published at GET /v1/shield/devices/.well-known/jwks.json (public).
Filtering profiles
/v1/shield/devices/{deviceId}/doh_profileA signed Apple .mobileconfig (application/x-apple-aspen-config) that points the device at https://{short_code}.dns.heyvalue.com/dns-query. It is signed with the tenant's own DoH certificate, so the signer identity matches the brand domain; issuance fails if no signer exists for the domain.
/v1/shield/devices/{deviceId}/vpn_profileA WireGuard .conf (attachment) for a DNS-only split tunnel. The peer endpoint is the tenant apex dns.heyvalue.com:51820 (anycast), DNS pointed at the in-tunnel resolver.
[Interface]
Address = 10.x.y.z/32
DNS = 10.7.0.1
PrivateKey = <device private key>
[Peer]
PublicKey = <WG_SERVER_PUBLIC_KEY>
Endpoint = dns.heyvalue.com:51820
AllowedIPs = 10.7.0.0/16
PersistentKeepalive = 25Per-device policy
All paths are under /v1/shield/devices/{deviceId}. Category/application/rule changes reset the device's schedule-applied flag.
| Method | Path | Purpose |
|---|---|---|
| POST | …/filtering | Toggle device filtering ({ enabled, duration? }) |
| GET POST | …/categories · …/categories/{catId} | List / toggle content categories |
| GET POST | …/applications · …/applications/{appId} | List / toggle per-app blocking |
| GET POST DEL | …/rules · …/rules/{rid} | Custom allow/block rules (value, type: whitelist|blacklist) |
| GET POST | …/ips | Map static client IPs (IPv4) to the device |
| GET POST | …/safe_search · …/safe_search/{provider} | Enforce provider safe-search |
| POST | …/internet_access | Suspend/restore device internet access |
| POST | …/scheduler | Enable/disable time-based schedules |
Live & simulations
| Method | Path | Purpose |
|---|---|---|
| GET | …/live | Server-Sent Events: snapshot then 1s ticks of query activity |
| POST | …/simulations | Queue a DNS-resolution simulation ({ "domain": "…" }) |
| GET | …/simulations/active · …/simulations/{simId} | Active / specific simulation result |
Data models
Profile & ServiceStatus
The company's Profile is the source of truth for subscription state; ServiceStatus (returned by GET /service) is a computed projection of it.
| ServiceStatus field | Type | Derived as |
|---|---|---|
plan | string | Profile plan name |
enabled | bool | not disabled |
filtering_active | bool | enabled & not suspended |
suspended_until | time? | set only for a bounded suspension |
internet_access_filtering_active | bool | enabled & internet not suspended |
internet_access_suspended_until | time? | set only for a bounded suspension |
Device
| Field | Type | Notes |
|---|---|---|
id, tenant_id, company_id | string | Identity + scope |
name, type, managed | … | type ∈ ios·android·windows·macos |
connection_method, short_code | string? | doh·dot·vpn; short code for DoH/DoT |
dns_provisioning_endpoint, dns_server_addrs | computed | Filled from tenant DNS config |
suspended_at, suspended_until | time? | Filtering suspension |
internet_access_suspended_at / _until | time? | Internet-access suspension |
public_jwk, heartbeat, metadata | object? | Device key; ClickHouse signals; free-form |
Reference
Plans
Plan names accepted by the Shield subscription (from the licensing catalog default):
HeyValue enables its own subset of these plans; the list above is the platform default. Category families map by prefix: p_ parental, f_ family, biz_ business, sch_ school, each with low, medium, and high tiers.
Safe-search providers
Device types & rule types
Health & ops
| Method | Path | Purpose |
|---|---|---|
| GET | / · /v1/ | Health — { "version": "v1", "status": "ok" } |
| GET | /metrics | Prometheus metrics |
HeyValue tenant fact sheet
Concrete configuration values for the HeyValue tenant, used throughout the examples above.
End-to-end: provisioning Shield for a HeyValue customer
A complete worked flow from a new customer to a filtered device.
- Create the company
POST /v1/companieswithtenant_id: pymeheyvalue. Licensing auto-provisions the company's entitlements on201. - Enable Shield with a plan
PUT /v1/licensing/companies/{cid}/services/shield/configwithenabled: trueandplans.current. Licensing calls ShieldPOST /v1/service. - Create a device link
POST /v1/shield/devices/link→ returns a 6-digit code the user enters in the HeyValue app. - Device enrollsThe app submits the device's P-256 public key to
POST /v1/shield/devices/link/{code}?tid=pymeheyvalue, then mints a token viaPOST /v1/shield/devices/token. - Deliver the filtering profile
GET …/doh_profile(signed fordns.heyvalue.com) or…/vpn_profile. The device is now filtered at the HeyValue anycast edge.
curl -X PUT https://api.heyvalue.com/v1/licensing/companies/$CID/services/shield/config \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"config": {
"name": "shield",
"available": true,
"enabled": true,
"limits": { "shield_devices_per_company": { "value": 5 } },
"features": { "shield_safe_search": { "enabled": true } },
"plans": { "available": ["basic","family_parental"], "current": "family_parental" }
}
}'