Skip to content

ZPIP — cross-product protocol

ZPIP (Zev Product Integration Protocol) is how Zev products call each other. From a compliance perspective it matters because it's the mechanism by which one product accesses another's data — including personal data — and the structure of the user's consent.

This page is the DPO-facing overview. The engineering-facing protocol spec is in zevid-backend/docs/product-integration/PROTOCOL.md (link to be set once internal URL is finalised).

Two flavours

Service-only

Generic ecosystem utilities, no user in the loop. Example: zevpay.banking.read (look up a Nigerian bank account name from a NUBAN). The lookup doesn't touch any specific user's account — it's a utility that any Zev product can call.

  • No consent screen.
  • No user data accessed.
  • Authenticated by service-key.
  • Audit log per call (zpip_token_log in ZevID).

A product acts on behalf of a specific user for a specific scope. Example: ZevCommerce calling ZevPay to set up Checkout API access for the user's business — uses zevpay.checkout.write on the user's behalf.

  • Requires explicit user consent the first time. The user sees a screen at accounts.zevop.com/zpip-consent and clicks Allow.
  • Consent is per (user × calling product × scope). Recorded in zpip_scope_grants in ZevID.
  • Revocable at any time from accounts.zevop.com → Connected Apps.
  • Every token issued is logged in zpip_token_log with the calling product, the audience, the scope, the user, and a JWT identifier (jti) for replay protection.
sequenceDiagram
  participant Caller as Calling product
  participant ZevID
  participant User as User's browser
  participant Target as Target product

  Caller->>ZevID: token-exchange (scope, user, return_to)
  alt user already granted
    ZevID-->>Caller: access_token
    Caller->>Target: API call with token
  else needs consent
    ZevID-->>Caller: { consent_required, consent_url }
    Caller->>User: redirect to consent_url
    User->>ZevID: load consent screen
    ZevID-->>User: render "Caller wants <scope>"
    User->>ZevID: Allow (per-scope checkboxes)
    ZevID-->>User: redirect to return_to with ?granted=…&denied=…
    User->>Caller: returns to product
    Caller->>ZevID: token-exchange (retry)
    ZevID-->>Caller: access_token
    Caller->>Target: API call with token
  end

To avoid users seeing N consent screens when a single user action needs N scopes, callers can pass scopes: string[] instead of scope: string. ZevID creates ONE consent screen covering every missing scope; user picks per-scope checkboxes; redirect carries ?granted=…&denied=….

Constraint: all scopes in a batch must share the owner-product (first dotted segment). Cross-product batches aren't allowed — each owner gets its own consent dialogue. Compliance rationale: keeps the consent narrative single-owner.

Decision record: zevid-backend/docs/decisions/0001-batched-zpip-consent.md (link to be set).

Lawful basis for ZPIP processing

  • Service-only calls: legitimate interest (operational necessity for the ecosystem to function). No personal data is exchanged.
  • Service + user calls: explicit consent (NDPA §25(1)(a)). The user's grant in zpip_scope_grants is the consent record.

Revocation

Users revoke at accounts.zevop.com → Connected Apps. On revocation:

  • zpip_scope_grants.revoked_at is set.
  • A user.consent.revoked webhook fires to the calling product.
  • Already-issued tokens (≤5 min TTL) continue working. Target products needing instant revocation use per-call introspection (POST /v1/internal/oauth/introspect).

Catalog

Every ZPIP capability registered by a product, with the scopes it requires, lives in the catalog (product_capabilities table). See zpip-catalog.md for the per-product listing.