ENFORCER

ENFORCER

VERSION v3
BASE /api/v1/enforcer
STATUS IN PRODUCTION
Enforcer v3, Developer onboarding

The security front desk for your app.

Enforcer handles who someone is and what they are allowed to do, so you do not stitch together five vendors to ship an app that touches money or personal data. This guide takes you from zero to a working tenant.

Trusted in live fintech and identity apps
00Watch
00

Watch

See how it works

A one minute animation of how Enforcer works, plain enough for anyone to follow. Captions are on, so it plays with the sound off too.

Enforcer, how it works1 min

No sound needed. The captions tell the whole story.

01Overview
01

Overview

What Enforcer is

Think of Enforcer as the front desk for your application. When someone walks in, the front desk checks who they are, login plus optional identity checks, and then decides what they are allowed to do once inside. Enforcer is that desk, delivered as a ready made backend you call over HTTP.

Most teams building anything with payments or personal data end up assembling separate tools for login, identity verification, permissions, and record level access. Each one has its own model, and keeping them in agreement is constant work. Enforcer replaces that pile with one policy engine. The same engine that decides "can this person open this record" also decides "which records can they even see", and a test proves the two never disagree.

02Model
02

Mental model

Core concepts

Five ideas cover almost everything you will do. Read these once and the rest of the guide reads quickly.

02.1

Tenant

A tenant is your app inside Enforcer. It owns its users, groups, roles, theme, and keys. One Enforcer deployment can host many tenants, fully isolated from each other.

02.2

Person vs user

Identity is two layers. A cross tenant person is the human. A per tenant user is their login in one app. A verified person carries across apps and tenant switches without re verifying.

02.3

Groups

Groups are how a capability gets unlocked. A user is added to a group, and that membership is what opens a route or action. Pass an identity check, join the verified group, and a previously forbidden action becomes allowed.

02.4

Auth methods

Several ways to prove who someone is: passkey, email OTP, phone OTP over SMS, Google, SIWE for wallets, and Privy. You pick which ones your tenant accepts.

The moat, in one line

One policy brain makes every permission decision. Deciding what a person may do and deciding what a person may see come from the same engine, and a test proves they never disagree. A stack stitched from separate vendors cannot give you that guarantee.

Modules

The base, identity plus permissions, is the whole front desk above. Heavier capabilities are paid modules you switch on per tenant:

  • Banking and KYC, identity verification and money movement, runs as an async module and reports back by webhook.
  • Messaging, sending email and SMS to your users.
  • Storage, files tied to a tenant and account.
  • Wallet, on chain wallet capability.

How KYC connects to access: KYC is a module, not a core route. When a user passes verification, the result of that step is that they get added to your verified group. From then on the policy engine lets them through the gated routes. That "passing KYC becomes a permission" step is the thing a glued together stack does not give you.

03Quickstart
03

Get going

Quickstart

The live sandbox is open, no setup needed. Base URL is https://api.instruxi.dev/api/v1/enforcer and the sandbox tenant code is 2OPX-BYIE-GDUH. Watch the 30 second walkthrough, run it yourself right here, or do it by hand below.

Watch, using the sandbox30 sec
Try it live

Walk into the front desk

A real sign in and a real access gate, shown as a little world. The only thing you type is your email, the code lands in your inbox.

This is the front desk for your app. Sign in to walk through.

    Prefer the terminal? The same calls by hand are right below.

    Sign in to the sandbox

    Real email, real token. Ask for a one time code, exchange it for a session token, then call the API with that token.

    1, request a code
    curl -X POST "https://api.instruxi.dev/api/v1/enforcer/auth/otp/request" \
      -H "Content-Type: application/json" \
      -d '{ "email": "you@example.com", "tenant_code": "2OPX-BYIE-GDUH" }'
    
    # a 6 digit OTP is emailed to you, valid for 10 minutes
    2, log in
    curl -X POST "https://api.instruxi.dev/api/v1/enforcer/auth/login" \
      -H "Content-Type: application/json" \
      -d '{ "provider": "email_otp", "email": "you@example.com", "otp": "123456", "tenant_code": "2OPX-BYIE-GDUH" }'
    
    # returns token (1 hour) plus a refresh_token
    3, you now have a session
    # the login response gives you data.token and data.account.id
    # send the token on every call
    curl "https://api.instruxi.dev/api/v1/enforcer/auth/me" \
      -H "Authorization: Bearer <token>"

    See the gate flip

    Groups are how a capability gets unlocked. Create one, add yourself, and you are through the gate. In the sandbox you do it by hand to watch it work. In production a KYC pass or your own policy adds people to the group automatically.

    1, create a group (the gate)
    # the sandbox is shared, so pick a name and slug that are your own
    curl -X POST "https://api.instruxi.dev/api/v1/enforcer/groups" \
      -H "Authorization: Bearer <token>" \
      -H "Content-Type: application/json" \
      -d '{ "name": "Verified yourname", "slug": "verified-yourname", "description": "passed the check", "is_public": false }'
    
    # response: data.id is your new group id
    2, add yourself to it
    curl -X POST "https://api.instruxi.dev/api/v1/enforcer/groups/<group_id>/members" \
      -H "Authorization: Bearer <token>" \
      -H "Content-Type: application/json" \
      -d '{ "account_id": "<your account id from login>" }'
    3, confirm you are through
    curl "https://api.instruxi.dev/api/v1/enforcer/groups/<group_id>/members" \
      -H "Authorization: Bearer <token>"
    
    # your account is in the member list. that membership is the gate.

    That is the whole idea. Group membership flips a capability from blocked to allowed with no code change on the route. Swap the manual add for a KYC pass and you have the production flow.

    refresh the token when it expires
    curl -X POST "https://api.instruxi.dev/api/v1/enforcer/auth/refresh" \
      -H "Content-Type: application/json" \
      -d '{ "refresh_token": "<refresh_token>" }'

    Two ways to build on it:

    The fastest path

    Inside an AI coding tool such as Cursor, Claude Code, Lovable, or Windsurf, you describe the app you want and the Enforcer connector provisions a real tenant and scaffolds a working app wired to it.

    prompt
    build me a banking app with login and KYC

    The connector creates a tenant, mints the keys, wires up the auth methods you asked for, and leaves you with code that already talks to Enforcer. From there you keep building in plain language or drop down to the API below.

    The connector is the front door for new builds. When you need exact control, the by hand path uses the same endpoints under the hood.

    The sandbox above signs you into a shared tenant. To stand up your own tenant and gate an action, here is the full sequence, ask your Enforcer contact to enable self serve for your account. Your base URL is https://api.instruxi.dev (live now). Replace the placeholder values as you go. Values like $TENANT_CODE, $GROUP_ID and $ACCOUNT_ID come from the responses of earlier steps, noted inline below. Single quoted JSON does not expand shell variables, so substitute the real values by hand when you run these.

    1. Create your tenant, no human in the loop

      Self serve tenant creation makes the caller the tenant admin and hands back the new tenant. The response carries the tenant (note its id and code) and your tenant admin session token. Use that token as $ADMIN_JWT and the tenant code as $TENANT_CODE below. Confirm the exact response field names against your staging spec.

      request
      curl -X POST "$ENFORCER_BASE_URL/api/v1/enforcer/tenants/self-serve" \
        -H "Content-Type: application/json" \
        -d '{
          "name": "Acme Pay",
          "admin_email": "you@acme.com"
        }'
      
      # response: the new tenant (id, code) plus your tenant-admin session token
    2. Mint a server API key

      Use the tenant admin token from the step above as the bearer. The plaintext key is shown once, store it now.

      request
      curl -X POST "$ENFORCER_BASE_URL/api/v1/enforcer/api-keys" \
        -H "Authorization: Bearer $ADMIN_JWT" \
        -H "Content-Type: application/json" \
        -d '{ "name": "server-key" }'
    3. Register a user and request an OTP

      Register the account, then request an email OTP scoped to your tenant. In development the OTP is returned to you so you can script the flow.

      request
      # register
      curl -X POST "$ENFORCER_BASE_URL/api/v1/enforcer/auth/register" \
        -H "Content-Type: application/json" \
        -d '{ "provider": "email_otp", "email": "user@acme.com" }'
      
      # request the OTP
      curl -X POST "$ENFORCER_BASE_URL/api/v1/enforcer/auth/otp/request" \
        -H "Content-Type: application/json" \
        -d '{ "email": "user@acme.com", "tenant_code": "$TENANT_CODE" }'
    4. Log in to get a user JWT

      Exchange the OTP for a user bearer token plus a refresh token.

      request
      curl -X POST "$ENFORCER_BASE_URL/api/v1/enforcer/auth/login" \
        -H "Content-Type: application/json" \
        -d '{
          "provider": "email_otp",
          "email": "user@acme.com",
          "otp": "123456",
          "tenant_code": "$TENANT_CODE"
        }'
      
      # response includes: token, refresh_token, expires_at, account
    5. Add the user to the gating group

      The group is the capability gate. Membership is what unlocks the route the group protects. List your tenant's groups to find the one you want to gate on (its id is $GROUP_ID), then add the account ($ACCOUNT_ID from the login response above). The API key goes in the X-API-Key header. Groups are provisioned with your tenant, confirm with your Enforcer contact how your gating groups are seeded.

      request
      # list this tenant's groups, take the id of the one you gate on
      curl "$ENFORCER_BASE_URL/api/v1/enforcer/groups" \
        -H "X-API-Key: $API_KEY"
      
      # add the account to it -> capability now unlocked
      curl -X POST "$ENFORCER_BASE_URL/api/v1/enforcer/groups/$GROUP_ID/members" \
        -H "X-API-Key: $API_KEY" \
        -H "Content-Type: application/json" \
        -d '{ "account_id": "$ACCOUNT_ID" }'

    That last step is the whole point. The user existed and could log in, but the gated action was forbidden. Adding them to the group flips that to allowed, with no code change on the route. Swap the manual add for the KYC module reporting a pass and you have the production verification flow.

    04Recipes
    04

    How to

    Recipes

    Short, copyable answers to the things you will do in the first week.

    Gate a page on KYC

    This is the core mechanic. The route is protected by group membership. A new user is not in the group, so the action is forbidden. When the KYC module reports a pass, you add the account to the verified group and the same route now allows them.

    request
    # on KYC pass, add the account to the gating group
    curl -X POST "$ENFORCER_BASE_URL/api/v1/enforcer/groups/$GROUP_ID/members" \
      -H "X-API-Key: $API_KEY" \
      -d '{ "account_id": "$ACCOUNT_ID" }'
    
    # to revoke access later, remove them
    curl -X DELETE "$ENFORCER_BASE_URL/api/v1/enforcer/groups/$GROUP_ID/members/$ACCOUNT_ID" \
      -H "X-API-Key: $API_KEY"

    The route itself does not change. You are only changing who is in the group.

    Add a login method

    Enforcer supports passkey, email OTP, phone OTP over SMS, Google, SIWE, and Privy. The provider field on login and register selects the method. Email OTP login looks like this:

    request
    curl -X POST "$ENFORCER_BASE_URL/api/v1/enforcer/auth/login" \
      -d '{ "provider": "email_otp", "email": "user@acme.com", "otp": "123456", "tenant_code": "$TENANT_CODE" }'

    For Google or Privy you pass the provider token in the token field instead of an OTP. Set which method your tenant accepts with the dedicated auth-provider call:

    request
    curl -X PUT "$ENFORCER_BASE_URL/api/v1/enforcer/tenants/$TENANT_ID/auth-provider" \
      -H "Authorization: Bearer $ADMIN_JWT" \
      -H "Content-Type: application/json" \
      -d '{ "provider": "email_otp" }'

    Confirm the exact passkey and SIWE begin and finish routes against the live spec for your build.

    Invite teammates

    Create an invite for your tenant, then the invitee accepts it to join.

    request
    # create an invite (tenant admin)
    curl -X POST "$ENFORCER_BASE_URL/api/v1/enforcer/tenants/$TENANT_ID/invites" \
      -H "Authorization: Bearer $ADMIN_JWT" \
      -d '{ "email": "teammate@acme.com" }'
    
    # invitee accepts
    curl -X POST "$ENFORCER_BASE_URL/api/v1/enforcer/auth/invites/accept" \
      -H "Authorization: Bearer $USER_JWT" \
      -d '{ "code": "$INVITE_CODE" }'

    To set what a teammate can do, assign them a role with PUT /accounts/{id}/role. List the available roles with GET /roles.

    Theme your app

    Set the tenant name, logo, and theme with a single patch.

    request
    curl -X PATCH "$ENFORCER_BASE_URL/api/v1/enforcer/tenants/$TENANT_ID" \
      -H "Authorization: Bearer $ADMIN_JWT" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "Acme Pay",
        "logo_url": "https://acme.com/logo.png",
        "theme": { "primary": "#6ce992" }
      }'
    Switch a user between tenants

    Because the person sits above per tenant users, one human can hold memberships in several tenants. List them, then switch. A switch returns a token scoped to the new tenant.

    request
    # list this person's tenant memberships
    curl "$ENFORCER_BASE_URL/api/v1/enforcer/auth/tenants" \
      -H "Authorization: Bearer $USER_JWT"
    
    # switch into another tenant
    curl -X POST "$ENFORCER_BASE_URL/api/v1/enforcer/auth/tenant/switch" \
      -H "Authorization: Bearer $USER_JWT" \
      -d '{ "tenant_id": "$OTHER_TENANT_ID" }'

    To join a tenant the person is not yet a member of, use POST /auth/tenant/join with a tenant_code.

    05Credentials
    05

    Credentials

    Auth model

    Three kinds of credential reach the API. Pick by who is calling.

    CredentialWhere it comes fromUse it for
    User JWT Returned by POST /auth/login as token User scoped calls. Anything done as the logged in person, reading their own data, switching tenants, accepting an invite.
    API key Returned by POST /api-keys, plaintext shown once Server and admin calls from your backend. Creating groups, adding members, managing accounts. Keep it server side, never ship it to a browser.
    Privy access token From your Privy integration Pass it directly as the bearer. Enforcer accepts a Privy access token in place of a user JWT.

    All three travel in the same header: Authorization: Bearer <value>. Refresh an expiring user JWT with POST /auth/refresh using its refresh_token.

    06Plans
    06

    Plans

    Modules and pricing

    Pricing works like a phone plan. There is a base everyone pays, then add on modules you switch on per tenant and pay for by usage. You can start on the base for free.

    06.1

    Base, free to start

    Identity and permissions, the whole front desk. Tenants, persons and users, groups, roles, all the auth methods, and the one policy engine. This is everything in the concepts and quickstart above.

    06.2

    Add on modules, metered

    • Banking and KYC, verification and money movement
    • Messaging, email and SMS
    • Storage, tenant scoped files
    • Wallet, on chain wallet capability

    You only pay for a module once you turn it on, and the charge follows usage. For current rates and to enable a module on your tenant, talk to your Enforcer contact.

    07Reference
    07

    Reference

    API reference

    The browsable Swagger UI and the machine readable spec are live now. Point your tooling at:

    endpoints
    # browsable Swagger UI
    https://api.instruxi.dev/api/v1/enforcer/swagger
    
    # raw OpenAPI spec (JSON)
    https://api.instruxi.dev/api/v1/enforcer/swagger/doc.json
    
    # base path for every endpoint
    https://api.instruxi.dev/api/v1/enforcer

    The endpoints you will reach for most often, grouped by area. Treat the live Swagger UI as the authority for exact request and response shapes, this list is the map.

    Tenants

    POST/tenants/self-servecreate a tenant, caller becomes tenant admin
    POST/tenantscreate a tenant
    PATCH/tenants/{id}set name, logo, theme
    PUT/tenants/{id}/auth-providerconfigure accepted login methods
    POST/tenants/{id}/invitesinvite a teammate
    GET/directory/tenants/resolveresolve a tenant in the directory

    Auth

    POST/auth/registercreate an account
    POST/auth/otp/requestemail or phone OTP
    POST/auth/loginreturns user JWT plus refresh token
    POST/auth/refreshrefresh an expiring token
    POST/auth/passkey/register/begin · /finishpasskey enrollment
    POST/auth/passkey/login/begin · /finishpasskey sign in
    POST/auth/siwe/noncesign in with Ethereum nonce
    POST/auth/tenant/joinjoin a tenant by code
    POST/auth/tenant/switchswitch active tenant
    GET/auth/tenantsthis person's memberships

    Groups, capability gating

    GET/groupslist groups
    POST/groupscreate a group
    POST/groups/{id}/membersadd a member, unlocks the capability
    DELETE/groups/{id}/members/{account_id}remove a member
    GET/directory/accounts/{id}/groupsgroups an account belongs to

    Accounts and roles

    GET/rolesrole catalog
    GET/accountslist accounts
    GET/accounts/{id}one account
    PUT/accounts/{id}/roleset an account's role
    PUT/accounts/{id}/activeactivate or deactivate

    API keys

    POST/api-keysmint a key, plaintext shown once
    GET/api-keyslist keys
    DELETE/api-keys/{id}delete a key
    08FAQ
    08

    Questions

    FAQ

    Can I self host Enforcer?

    Enforcer ships as a deployable backend and runs with high availability in production today. For self hosting terms and a deployment that fits your environment, talk to your Enforcer contact.

    Where does the data live?

    Each tenant's users, groups, roles, and keys are stored and isolated per tenant inside your Enforcer deployment. The cross tenant person layer is what lets a verified identity carry between tenants, while per tenant data stays separated. For the exact hosting region and data residency of your environment, check with your Enforcer contact.

    Is there a sandbox?

    Yes, work against a staging environment before production. There is not a public self signup sandbox URL in this guide, so ask your Enforcer contact for the staging base URL, then set it as $ENFORCER_BASE_URL and every example here works unchanged.

    JWT or API key, which do I send?

    Send a user JWT for anything done as a logged in person, and an API key for server side admin work like creating groups and adding members. A Privy access token can be sent directly as the bearer in place of a user JWT. See the Auth model section above.