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. Sign in with your email and start calling the API.

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 code 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, call the api
# send the token on every call
curl "https://api.instruxi.dev/api/v1/enforcer/auth/tenants" \
  -H "Authorization: Bearer <token>"
4, refresh 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>" }'

That is a real, working session against the live Enforcer sandbox. The two paths below show how to build on top of it.

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 code 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 code
    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/swagger

# raw OpenAPI spec (JSON)
https://api.instruxi.dev/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.