Provisioning API
v1

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:

request path
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 · shield

The 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:

EnvironmentBase URLIdentity realm
Production (HeyValue)https://api.heyvalue.compyme-heyvalue
Development clonehttps://api.digital-smb.devpyme-digitalsmb
Gateway path rewriting

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.

Issuer
https://auth.heyvalue.com/realms/pyme-heyvalue
Token endpoint
…/protocol/openid-connect/token
Audience (user)
https://api.heyvalue.com
Audience (M2M)
https://m2m.heyvalue.com
JWKS
…/openid-connect/certs
bash
curl https://api.heyvalue.com/v1/shield/service \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "X-TARGET-COMPANY-ID: 4c1e…company-uuid"

Three authentication planes

PlaneHow the caller authenticatesTenant context
End-user / integratorAuthorization: Bearer <JWT> from the HeyValue realmGateway 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 endpointsNone — device link/token/DoH are unauthenticatedTenant passed as ?tid=pymeheyvalue query
Identity headers are gateway-injected

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.

json · success
{
  "data":   { "…": "…" },
  "errors": null,
  "status": 200
}

Many mutating Shield endpoints return a bare boolean payload on success:

json
{ "data": true, "errors": null, "status": 200 }

Identity headers

HeaderMeaningSet by
X-TENANT-IDTenant / operator id (e.g. pymeheyvalue)Gateway (from JWT) or M2M caller
X-TARGET-COMPANY-IDThe customer company being acted onGateway (from JWT) or M2M caller
X-SUBJECT-IDActing user id — platform gateway header onlyGateway (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.

json · validation error (400)
{
  "data": null,
  "errors": [
    { "message": "non zero value required", "field": "company_id", "ref": "" }
  ],
  "status": 400
}
Shield: not-found returns 500

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.

StatusMeaning
200Success
201Resource created (device, rule, simulation)
204No content (no active/next simulation; authorize callback ack)
400Request bind / validation failure
403Rejected by the licensing / authorization check at the gateway
500Service 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.

  1. Create the company
    POST /v1/companies. The gateway's licensing check reserves any consumed entitlement and flags a post-response callback.
  2. Auto-provision entitlements
    On 201, the gateway calls licensing back; licensing runs resetCompany(company_id, tenant_id) — writing the company's per-service config and seeding limits from the tenant defaults.
  3. Enable a product
    An admin PUTs the company's Shield service config. Licensing then calls Shield: POST /v1/service {company_id, enabled, plan} to provision the subscription.
  4. Provision devices
    Create devices (or short-code links) under the company, then deliver a DoH or WireGuard profile to each device.
  5. Commit the reservation
    The gateway posts the upstream result to POST /v1/authorize/response; licensing confirms the reservation on success or rolls it back on failure.
Fail-closed

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.

POST/v1/companiesaccounts-api

Create a customer company. A 201 here is what triggers automatic licensing provisioning (see How provisioning works).

FieldTypeReqNotes
tenant_idstringrequirede.g. pymeheyvalue
reference_idstringrequiredExternal correlation id
namestringoptional3–255 chars; defaults to reference_id
owner_iduuidoptionalAdded as an owner membership
parent_company_iduuidoptionalParent in the org tree
email, phone, tax_id, addressoptionalContact / billing detail
bash
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"
  }'
json · 201 Created
{
  "id": "4c1e…",
  "tenant_id": "pymeheyvalue",
  "reference_id": "acme-001",
  "name": "Acme Ltd",
  "membership": [],
  "path": "…",
  "created_at": "2026-07-08T10:00:00Z"
}
POST/v1/accountsaccounts-api

Create 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.

FieldTypeReqNotes
tenant_idstringrequiredSelects the realm/client
emailemailrequiredLogin identity
passwordstringoptionalAuto-generated (16 chars) if omitted
first_name, last_name, phone, localeoptionalphone is E.164
tenant_membershipsstring[]optionalExtra 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

MethodPathPurpose
GET/v1/licensing/services/configDefault service catalog
GET PUT/v1/licensing/tenants/{tid}/services/configPer-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}/configPer-company, per-service config
GET/v1/licensing/companies/{cid}/services/usageLimit 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:

json · shield service config
{
  "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"
  }
}
Effect

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.

Authorize (data plane)

These endpoints are called by the gateway, not by integrators — documented for completeness.

MethodPathPurpose
ALL/v1/authorize?path={original}Phase 1: reserve + decide. Returns x-authz-licensing-status: allowed|rejected
POST/v1/authorize/responsePhase 2: commit or roll back the reservation from the upstream result

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.

GET/v1/shield/service

Return the full provisioning status for the company in X-TARGET-COMPANY-ID.

json · 200 · data
{
  "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.

POST/v1/shield/service

Enroll a company or update its subscription — set/change the plan and/or enable/disable the service. A first-time enrollment requires X-TENANT-ID.

FieldTypeReqNotes
company_idstringrequiredTarget company
planstringoptionalA plan name; omit to leave unchanged
enabledbooloptionalOmit to leave unchanged
bash
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" }'
Plan change re-provisions devices

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.

DELETE/v1/shield/service

Completely 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

GET/v1/shield/service/filtering

Trimmed, console-facing view: { "enabled": <filtering_active>, "plan": "…", "suspended_until": "…"? }.

POST/v1/shield/service/filtering

Suspend or resume filtering for the company (header X-TARGET-COMPANY-ID).

FieldTypeNotes
enabledbooltrue resumes, false suspends
durationstringOptional Go duration (e.g. "2h") — auto-resume window when suspending

Internet access

GET/v1/shield/service/internet_access
POST/v1/shield/service/internet_access

Suspend 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.

POST/v1/shield/devices

Create 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.

FieldTypeReqNotes
namestringrequired3–255 chars
typeenumrequiredios · android · windows · macos
connection_methodenumoptionaldoh · dot · vpn
managedbooloptionalManaged client flag
json · 201 · Device (abridged)
{
  "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

MethodPathNotes
GET/v1/shield/devices?include=heartbeatList 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/resetReset heartbeat
POST/v1/shield/devices/tokenpublic

Exchange 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.

json · request
{
  "device_id": "dev-uuid",
  "timestamp": 1751971200,
  "signature": "base64url(R||S)"
}
json · 200
{ "token": "eyJhbGciOiJFUzI1Ni…" }

Verification keys are published at GET /v1/shield/devices/.well-known/jwks.json (public).

Filtering profiles

GET/v1/shield/devices/{deviceId}/doh_profile

A 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.

GET/v1/shield/devices/{deviceId}/vpn_profile

A 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.

ini · wireguard .conf
[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 = 25

Per-device policy

All paths are under /v1/shield/devices/{deviceId}. Category/application/rule changes reset the device's schedule-applied flag.

MethodPathPurpose
POST…/filteringToggle 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…/ipsMap static client IPs (IPv4) to the device
GET POST…/safe_search · …/safe_search/{provider}Enforce provider safe-search
POST…/internet_accessSuspend/restore device internet access
POST…/schedulerEnable/disable time-based schedules

Live & simulations

MethodPathPurpose
GET…/liveServer-Sent Events: snapshot then 1s ticks of query activity
POST…/simulationsQueue 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 fieldTypeDerived as
planstringProfile plan name
enabledboolnot disabled
filtering_activeboolenabled & not suspended
suspended_untiltime?set only for a bounded suspension
internet_access_filtering_activeboolenabled & internet not suspended
internet_access_suspended_untiltime?set only for a bounded suspension

Device

FieldTypeNotes
id, tenant_id, company_idstringIdentity + scope
name, type, managedtype ∈ ios·android·windows·macos
connection_method, short_codestring?doh·dot·vpn; short code for DoH/DoT
dns_provisioning_endpoint, dns_server_addrscomputedFilled from tenant DNS config
suspended_at, suspended_untiltime?Filtering suspension
internet_access_suspended_at / _untiltime?Internet-access suspension
public_jwk, heartbeat, metadataobject?Device key; ClickHouse signals; free-form

Reference

Plans

Plan names accepted by the Shield subscription (from the licensing catalog default):

basicfamily_parentalbusiness_productivity faithguardparentalschool
HeyValue plan set

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

googleyoutubeyoutube_moderate bingduckduckgoyandex

Device types & rule types

iosandroidwindowsmacos rule: whitelistrule: blacklist

Health & ops

MethodPathPurpose
GET/ · /v1/Health — { "version": "v1", "status": "ok" }
GET/metricsPrometheus metrics

HeyValue tenant fact sheet

Concrete configuration values for the HeyValue tenant, used throughout the examples above.

Tenant id (X-TENANT-ID)
pymeheyvalue
Realm / deploy slug
pyme-heyvalue
API base
https://api.heyvalue.com
Auth issuer
auth.heyvalue.com/realms/pyme-heyvalue
DoH / DoT domain
dns.heyvalue.com
Block page
blocked.heyvalue.com
Console
admin.heyvalue.com
Client download
download.heyvalue.com
Mobile apps
com.heyvalue.shield · HeyShield (iOS)

End-to-end: provisioning Shield for a HeyValue customer

A complete worked flow from a new customer to a filtered device.

  1. Create the company
    POST /v1/companies with tenant_id: pymeheyvalue. Licensing auto-provisions the company's entitlements on 201.
  2. Enable Shield with a plan
    PUT /v1/licensing/companies/{cid}/services/shield/config with enabled: true and plans.current. Licensing calls Shield POST /v1/service.
  3. Create a device link
    POST /v1/shield/devices/link → returns a 6-digit code the user enters in the HeyValue app.
  4. Device enrolls
    The app submits the device's P-256 public key to POST /v1/shield/devices/link/{code}?tid=pymeheyvalue, then mints a token via POST /v1/shield/devices/token.
  5. Deliver the filtering profile
    GET …/doh_profile (signed for dns.heyvalue.com) or …/vpn_profile. The device is now filtered at the HeyValue anycast edge.
bash · enable Shield (family plan)
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" }
    }
  }'