Platform Users API
Platform user routes let operators provision users across tenants, resolve
identity records, and attach immutable tenant-scoped wallet external IDs. They
require a platform key and route scopes when scoped platform keys are enabled.
curl https://api.steward.fi/platform/users/lookup \
-H "X-Steward-Platform-Key: your-platform-key"
{
userId: string;
email: string | null;
emailVerified: boolean | null;
name: string | null;
image: string | null;
walletAddress: string | null;
walletChain: string | null;
customMetadata: Record<string, unknown>;
deactivatedAt: string | null;
createdAt: string;
updatedAt: string;
tenantIds: string[];
linkedAccounts: Array<{
id: string;
provider: string;
providerAccountId: string;
expiresAt: number | null;
}>;
walletExternalIds: Array<{
tenantId: string;
externalId: string;
}>;
}
Wallet external IDs are write-once per user and tenant. Once assigned, the
value cannot be changed to a different external ID for that same user in that
tenant.
Create or Pre-Provision User
Creates a user record without sending email or requiring an interactive login.
If the email already exists, Steward returns the existing user with
isNew: false.
Auth: Platform key with platform:user:write
Request Body:
{
email: string;
emailVerified?: boolean;
name?: string;
customMetadata?: Record<string, unknown>;
tenantId?: string;
walletExternalId?: string; // or externalId
}
When walletExternalId is supplied, tenantId is required. Steward links the
user into that tenant and assigns the immutable wallet external ID in the same
operation.
const user = await steward.platformUsers.create({
email: "ada@example.com",
emailVerified: true,
tenantId: "app-prod",
walletExternalId: "wallet_ada_001",
});
Response:
{
"ok": true,
"data": {
"userId": "usr_123",
"isNew": true,
"tenantId": "app-prod",
"walletExternalId": "wallet_ada_001"
}
}
Lookup User
Looks up a user by a supported identity. walletExternalId lookup requires
tenantId because external IDs are tenant-scoped.
GET /platform/users/lookup
Auth: Platform key with platform:user:read
Query Parameters:
{
email?: string;
phone?: string; // E.164
walletAddress?: string;
walletExternalId?: string;
smartWalletId?: string;
customAuthId?: string;
provider?: string;
providerAccountId?: string;
tenantId?: string;
}
const { user } = await steward.platformUsers.lookup({
tenantId: "app-prod",
walletExternalId: "wallet_ada_001",
});
const sameUser = await steward.platformUsers.getUserByWalletExternalId(
"wallet_ada_001",
{ tenantId: "app-prod" }
);
Response:
{
"ok": true,
"data": {
"user": {
"userId": "usr_123",
"tenantIds": ["app-prod"],
"linkedAccounts": [],
"walletExternalIds": [
{ "tenantId": "app-prod", "externalId": "wallet_ada_001" }
]
}
}
}
Lookup Aliases
These POST aliases accept JSON bodies and return the same shape as
GET /platform/users/lookup.
| Endpoint | Body |
|---|
POST /platform/users/email/address | { "email": string, "tenantId"?: string } |
POST /platform/users/phone/number | { "phone": string, "tenantId"?: string } |
POST /platform/users/wallet/address | { "walletAddress": string, "tenantId"?: string } |
POST /platform/users/wallet/external-id | { "walletExternalId": string, "tenantId": string } |
POST /platform/users/smart-wallet/address | { "smartWalletId": string, "tenantId"?: string } |
Assign Wallet External ID
Assigns a write-once wallet external ID to an existing user in a tenant.
POST /platform/users/:userId/wallet/external-id
Auth: Platform key with platform:user:write
{
tenantId: string;
walletExternalId: string; // or externalId
}
await steward.platformUsers.assignWalletExternalId("usr_123", {
tenantId: "app-prod",
walletExternalId: "wallet_ada_001",
});
Conflicts return 409 when the external ID belongs to another user in the same
tenant or when the target user already has a different external ID in that
tenant.
Resolve Wallet External ID
Resolves a tenant-scoped wallet external ID.
POST /platform/users/wallet/external-id
Auth: Platform key with platform:user:read
{
tenantId: string;
walletExternalId: string; // or externalId
}
const { user } = await steward.platformUsers.resolveWalletExternalId({
tenantId: "app-prod",
walletExternalId: "wallet_ada_001",
});
Connect or Create by Wallet External ID
Resolves a wallet external ID to an existing user. If no mapping exists, Steward
can create a user or connect an existing email user to the tenant, then assign
the external ID.
POST /platform/users/wallet/external-id/connect-or-create
Auth: Platform key with platform:user:write
{
tenantId: string;
walletExternalId: string; // or externalId
email?: string;
emailVerified?: boolean;
name?: string;
customMetadata?: Record<string, unknown>;
role?: "owner" | "admin" | "member";
}
const result = await steward.platformUsers.connectOrCreateByWalletExternalId({
tenantId: "app-prod",
walletExternalId: "wallet_ada_001",
email: "ada@example.com",
emailVerified: true,
});
Response:
{
"ok": true,
"data": {
"userId": "usr_123",
"isNew": false,
"createdExternalId": true,
"tenantId": "app-prod",
"walletExternalId": "wallet_ada_001",
"user": {
"userId": "usr_123",
"tenantIds": ["app-prod"],
"walletExternalIds": [
{ "tenantId": "app-prod", "externalId": "wallet_ada_001" }
]
}
}
}
Search Tenant Users by External ID
Tenant-scoped user search supports direct wallet external ID filtering.
GET /platform/tenants/:tenantId/users?walletExternalId=wallet_ada_001
Auth: Platform key with platform:tenant-user:read
const result = await steward.platformUsers.search("app-prod", {
walletExternalId: "wallet_ada_001",
});
Tenant admins with a user session can use the same filter through the
user-authenticated directory route:
const result = await steward.listTenantUsers("app-prod", {
walletExternalId: "wallet_ada_001",
});
Third-Party Wallet Policy Violations
When restrictToOneThirdPartyWallet is enabled after existing users already
linked multiple EVM/Solana third-party wallets, tenant admins can review the
violation report and remove one selected wallet from a tenant member.
GET /user/me/tenants/:tenantId/users/wallet-policy/violations
Auth: User bearer token for a tenant admin, with recent MFA.
const report = await steward.getTenantWalletPolicyViolations("app-prod", {
limit: 50,
});
Response:
{
"ok": true,
"data": {
"tenantId": "app-prod",
"policyEnabled": true,
"violations": [
{
"userId": "usr_123",
"email": "ada@example.com",
"name": "Ada",
"role": "member",
"walletCount": 2,
"wallets": [
{
"accountId": "acct_1",
"provider": "wallet:ethereum",
"providerAccountId": "0x1111111111111111111111111111111111111111"
},
{
"accountId": "acct_2",
"provider": "wallet:solana",
"providerAccountId": "So11111111111111111111111111111111111111112"
}
]
}
],
"total": 1,
"limit": 50,
"offset": 0
}
}
The report is read-only. Use the remediation endpoint below when an owner/admin
has chosen the wallet identity to remove.
DELETE /user/me/tenants/:tenantId/users/:userId/wallet-policy/wallets/:accountId
Auth: User bearer token for a tenant owner/admin, with recent MFA and a
session scoped to the tenant.
The endpoint only removes linked EVM/Solana third-party wallets, refuses to
remove the user’s last remaining login method, revokes the remediated user’s
refresh tokens, writes authorized and final audit events, and sends a redacted
user.unlinked_account webhook.
const result = await steward.remediateTenantWalletPolicyViolation(
"app-prod",
"usr_123",
"acct_2",
);
Response:
{
"ok": true,
"data": {
"deleted": true,
"accountId": "acct_2",
"provider": "wallet:solana",
"providerAccountId": "So11111111111111111111111111111111111111112",
"issuedBefore": 1780613242000
}
}
POST /user/me/tenants/:tenantId/users/wallet-policy/remediations
Auth: User bearer token for a tenant owner/admin, with recent MFA and a
session scoped to the tenant.
The request accepts up to 50 selected wallet accounts. Each item is processed
with the same checks as the single-wallet endpoint: tenant membership, EVM or
Solana wallet provider, last-login-method protection, refresh-token revocation,
authorized/final audit events, and redacted user.unlinked_account webhooks.
The response is intentionally per-item so operators can retry only failed
selections.
const result = await steward.bulkRemediateTenantWalletPolicyViolations(
"app-prod",
[
{ userId: "usr_123", accountId: "acct_2" },
{ userId: "usr_456", accountId: "acct_7" },
],
);
Response:
{
"ok": true,
"data": {
"tenantId": "app-prod",
"succeeded": 1,
"failed": 1,
"results": [
{
"ok": true,
"targetUserId": "usr_123",
"deleted": true,
"accountId": "acct_2",
"provider": "wallet:solana",
"providerAccountId": "So11111111111111111111111111111111111111112",
"issuedBefore": 1780613242
},
{
"ok": false,
"targetUserId": "usr_456",
"accountId": "acct_7",
"status": 409,
"error": "Cannot unlink the user's last login method"
}
]
}
}
Wallet External ID Rules
tenantId is required for wallet external ID lookup and assignment.
- External IDs are trimmed, validated, and tenant-scoped.
- The same external ID cannot belong to multiple users in one tenant.
- A user cannot replace an existing wallet external ID with a different value in
the same tenant.
- The reserved backing provider used to store wallet external IDs is hidden
from
linkedAccounts and exposed through walletExternalIds.