ENFORCER
BASE /api/v1/enforcer
STATUS IN PRODUCTION
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.
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.
No sound needed. The captions tell the whole story.
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.
Mental model
Core concepts
Five ideas cover almost everything you will do. Read these once and the rest of the guide reads quickly.
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.
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.
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.
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.
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.
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
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
# send the token on every call curl "https://api.instruxi.dev/api/v1/enforcer/auth/tenants" \ -H "Authorization: Bearer <token>"
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.
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.
-
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
idandcode) and your tenant admin session token. Use that token as$ADMIN_JWTand the tenant code as$TENANT_CODEbelow. Confirm the exact response field names against your staging spec.requestcurl -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
-
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.
requestcurl -X POST "$ENFORCER_BASE_URL/api/v1/enforcer/api-keys" \ -H "Authorization: Bearer $ADMIN_JWT" \ -H "Content-Type: application/json" \ -d '{ "name": "server-key" }'
-
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" }'
-
Log in to get a user JWT
Exchange the OTP for a user bearer token plus a refresh token.
requestcurl -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
-
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
idis$GROUP_ID), then add the account ($ACCOUNT_IDfrom the login response above). The API key goes in theX-API-Keyheader. 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.
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.
# 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:
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:
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.
# 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.
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.
# 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.
Credentials
Auth model
Three kinds of credential reach the API. Pick by who is calling.
| Credential | Where it comes from | Use 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.
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.
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.
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.
Reference
API reference
The browsable Swagger UI and the machine readable spec are live now. Point your tooling at:
# 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
Auth
Groups, capability gating
Accounts and roles
API keys
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.