Docs Guides Managing End Users This guide walks through the full lifecycle of managing end users in Weavz — from registering a user to executing actions with their connections.
End users live within workspaces. Start by creating a workspace and configuring its integrations.
curl TypeScript SDK Python SDK TypeScript Python
# Create a workspace
curl -X POST https://api.weavz.io/api/v1/workspaces \
-H "Authorization: Bearer wvz_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "My App", "slug": "my-app"}'
# Add Slack with per-user connections
curl -X POST https://api.weavz.io/api/v1/workspaces/{workspaceId}/integrations \
-H "Authorization: Bearer wvz_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"integrationName": "slack",
"connectionStrategy": "per_user"
}' import { WeavzClient } from '@weavz/sdk'
const client = new WeavzClient ({ apiKey: 'wvz_your_api_key' })
// Create a workspace
const { workspace } = await client.workspaces. create ({
name: 'My App' ,
slug: 'my-app' ,
})
// Add Slack with per-user connections
await client.workspaces. addIntegration (workspace.id, {
integrationName: 'slack' ,
connectionStrategy: 'per_user' ,
}) from weavz_sdk import WeavzClient
client = WeavzClient( api_key = "wvz_your_api_key" )
# Create a workspace
result = client.workspaces.create( name = "My App" , slug = "my-app" )
workspace = result[ "workspace" ]
# Add Slack with per-user connections
client.workspaces.add_integration(workspace[ "id" ],
integration_name = "slack" ,
connection_strategy = "per_user" ,
) // Create a workspace
const wsRes = await fetch ( 'https://api.weavz.io/api/v1/workspaces' , {
method: 'POST' ,
headers: {
'Authorization' : 'Bearer wvz_your_api_key' ,
'Content-Type' : 'application/json' ,
},
body: JSON . stringify ({ name: 'My App' , slug: 'my-app' }),
})
const { workspace } = await wsRes. json ()
// Add Slack with per-user connections
await fetch ( `https://api.weavz.io/api/v1/workspaces/${ workspace . id }/integrations` , {
method: 'POST' ,
headers: {
'Authorization' : 'Bearer wvz_your_api_key' ,
'Content-Type' : 'application/json' ,
},
body: JSON . stringify ({
integrationName: 'slack' ,
connectionStrategy: 'per_user' ,
}),
}) import httpx
headers = { "Authorization" : "Bearer wvz_your_api_key" }
# Create a workspace
res = httpx.post(
"https://api.weavz.io/api/v1/workspaces" ,
headers = headers,
json = { "name" : "My App" , "slug" : "my-app" },
)
workspace = res.json()[ "workspace" ]
# Add Slack with per-user connections
httpx.post(
f "https://api.weavz.io/api/v1/workspaces/ { workspace[ 'id' ] } /integrations" ,
headers = headers,
json = {
"integrationName" : "slack" ,
"connectionStrategy" : "per_user" ,
},
)
When a new customer signs up for your product, register them as an end user. Use their ID from your system as the externalId.
curl TypeScript SDK Python SDK TypeScript Python
curl -X POST https://api.weavz.io/api/v1/end-users \
-H "Authorization: Bearer wvz_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"workspaceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"externalId": "user_123",
"displayName": "Alice Johnson",
"email": "[email protected] "
}' const { endUser } = await client.endUsers. create ({
workspaceId: workspace.id,
externalId: 'user_123' ,
displayName: 'Alice Johnson' ,
email: '[email protected] ' ,
}) result = client.end_users.create(
workspace_id = workspace[ "id" ],
external_id = "user_123" ,
display_name = "Alice Johnson" ,
email = "[email protected] " ,
)
end_user = result[ "endUser" ] const res = await fetch ( 'https://api.weavz.io/api/v1/end-users' , {
method: 'POST' ,
headers: {
'Authorization' : 'Bearer wvz_your_api_key' ,
'Content-Type' : 'application/json' ,
},
body: JSON . stringify ({
workspaceId: workspace.id,
externalId: 'user_123' ,
displayName: 'Alice Johnson' ,
email: '[email protected] ' ,
}),
})
const { endUser } = await res. json () res = httpx.post(
"https://api.weavz.io/api/v1/end-users" ,
headers = headers,
json = {
"workspaceId" : workspace[ "id" ],
"externalId" : "user_123" ,
"displayName" : "Alice Johnson" ,
"email" : "[email protected] " ,
},
)
end_user = res.json()[ "endUser" ]
Create a connect token so the end user can connect their Slack account through the hosted connect portal.
Use the Weavz endUser.id UUID in /end-users/{endUserUuid} resource paths. When you later execute actions, pass your product user ID as endUserId; that value is the end user's externalId.
If the workspace contains multiple configured aliases for the same integration, pass workspaceIntegrationId instead of only integrationName.
curl TypeScript SDK Python SDK TypeScript Python
curl -X POST https://api.weavz.io/api/v1/end-users/{endUserUuid}/connect-token \
-H "Authorization: Bearer wvz_your_api_key" \
-H "Content-Type: application/json" \
-d '{"integrationName": "slack"}' const { connectUrl } = await client.endUsers. createConnectToken (
endUser.id,
{ integrationName: 'slack' }
)
// Send connectUrl to your frontend to open in a popup
console. log ( 'Connect URL:' , connectUrl) result = client.end_users.create_connect_token(
end_user[ "id" ],
integration_name = "slack" ,
)
connect_url = result[ "connectUrl" ]
# Send connect_url to your frontend to open in a popup
print ( "Connect URL:" , connect_url) const res = await fetch (
`https://api.weavz.io/api/v1/end-users/${ endUser . id }/connect-token` ,
{
method: 'POST' ,
headers: {
'Authorization' : 'Bearer wvz_your_api_key' ,
'Content-Type' : 'application/json' ,
},
body: JSON . stringify ({ integrationName: 'slack' }),
}
)
const { connectUrl } = await res. json () res = httpx.post(
f "https://api.weavz.io/api/v1/end-users/ { end_user[ 'id' ] } /connect-token" ,
headers = headers,
json = { "integrationName" : "slack" },
)
connect_url = res.json()[ "connectUrl" ]
Open connectUrl in a popup or redirect the end user to it. The hosted connect page guides them through the OAuth2 consent flow. Once they approve, the connection is automatically linked to the end user.
Once the end user has connected their Slack, you can execute actions on their behalf by passing endUserId:
curl TypeScript SDK Python SDK TypeScript Python
curl -X POST https://api.weavz.io/api/v1/actions/execute \
-H "Authorization: Bearer wvz_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"integrationName": "slack",
"actionName": "send_channel_message",
"input": {
"channel": "C0123456789",
"text": "Hello from Alice!"
},
"workspaceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"endUserId": "user_123"
}' const result = await client.actions. execute ( 'slack' , 'send_channel_message' , {
workspaceId: workspace.id,
endUserId: 'user_123' ,
input: {
channel: 'C0123456789' ,
text: 'Hello from Alice!' ,
},
}) result = client.actions.execute(
"slack" ,
"send_channel_message" ,
workspace_id = workspace[ "id" ],
end_user_id = "user_123" ,
input = {
"channel" : "C0123456789" ,
"text" : "Hello from Alice!" ,
},
) const res = await fetch ( 'https://api.weavz.io/api/v1/actions/execute' , {
method: 'POST' ,
headers: {
'Authorization' : 'Bearer wvz_your_api_key' ,
'Content-Type' : 'application/json' ,
},
body: JSON . stringify ({
integrationName: 'slack' ,
actionName: 'send_channel_message' ,
input: { channel: 'C0123456789' , text: 'Hello from Alice!' },
workspaceId: workspace.id,
endUserId: 'user_123' ,
}),
})
const data = await res. json () res = httpx.post(
"https://api.weavz.io/api/v1/actions/execute" ,
headers = headers,
json = {
"integrationName" : "slack" ,
"actionName" : "send_channel_message" ,
"input" : { "channel" : "C0123456789" , "text" : "Hello from Alice!" },
"workspaceId" : workspace[ "id" ],
"endUserId" : "user_123" ,
},
)
data = res.json()
Weavz resolves Alice's Slack connection automatically based on the endUserId and the workspace's per_user connection strategy.
Instead of generating connect URLs programmatically, you can send email invitations directly:
curl TypeScript SDK Python SDK TypeScript Python
curl -X POST https://api.weavz.io/api/v1/end-users/{endUserUuid}/invite \
-H "Authorization: Bearer wvz_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected] ",
"integrationName": "slack"
}' await client.endUsers. invite (endUser.id, {
email: '[email protected] ' ,
integrationName: 'slack' ,
}) client.end_users.invite(
end_user[ "id" ],
email = "[email protected] " ,
integration_name = "slack" ,
) await fetch ( `https://api.weavz.io/api/v1/end-users/${ endUser . id }/invite` , {
method: 'POST' ,
headers: {
'Authorization' : 'Bearer wvz_your_api_key' ,
'Content-Type' : 'application/json' ,
},
body: JSON . stringify ({
email: '[email protected] ' ,
integrationName: 'slack' ,
}),
}) httpx.post(
f "https://api.weavz.io/api/v1/end-users/ { end_user[ 'id' ] } /invite" ,
headers = headers,
json = {
"email" : "[email protected] " ,
"integrationName" : "slack" ,
},
)
The email contains a link to the connect portal where Alice can authorize her Slack account.
Check which integrations an end user has connected:
curl TypeScript SDK Python SDK TypeScript Python
curl https://api.weavz.io/api/v1/end-users/{endUserUuid} \
-H "Authorization: Bearer wvz_your_api_key" const { endUser , connections } = await client.endUsers. get (endUser.id)
for ( const conn of connections) {
console. log ( `${ conn . integrationName }: ${ conn . status }` )
} result = client.end_users.get(end_user[ "id" ])
for conn in result[ "connections" ]:
print ( f " { conn[ 'integrationName' ] } : { conn[ 'status' ] } " ) const res = await fetch ( `https://api.weavz.io/api/v1/end-users/${ endUser . id }` , {
headers: { 'Authorization' : 'Bearer wvz_your_api_key' },
})
const { endUser , connections } = await res. json () res = httpx.get(
f "https://api.weavz.io/api/v1/end-users/ { end_user[ 'id' ] } " ,
headers = headers,
)
data = res.json()
connections = data[ "connections" ]
MCP OAuth is the preferred way to give agents end-user-scoped access when the MCP client can complete sign-in. The MCP server stays attached to the workspace, while each OAuth session resolves tool calls to a specific end user's connections. For provisioned clients that cannot run OAuth, use a bearer-enabled MCP server and issue one mcp_ bearer token per end user.
Use endUserAccess to control who can authorize the server:
restricted — only pre-existing workspace end users can authorize. Weavz matches the signed-in user by linked account or email and links the record when needed
open — a new end user can be created during MCP OAuth sign-in when the user is not already known
Create an end user Register the end user as shown in step 2 above.
Create an OAuth or bearer-enabled MCP server curl TypeScript SDK Python SDK TypeScript Python
curl -X POST https://api.weavz.io/api/v1/mcp/servers \
-H "Authorization: Bearer wvz_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Agent for Alice",
"workspaceId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"mode": "TOOLS",
"authMode": "oauth_and_bearer",
"endUserAccess": "restricted"
}' const { server , mcpEndpoint } = await client.mcpServers. create ({
name: 'Agent for Alice' ,
workspaceId: workspace.id,
mode: 'TOOLS' ,
authMode: 'oauth_and_bearer' ,
endUserAccess: 'restricted' ,
}) result = client.mcp_servers.create(
name = "Agent for Alice" ,
workspace_id = workspace[ "id" ],
mode = "TOOLS" ,
auth_mode = "oauth_and_bearer" ,
end_user_access = "restricted" ,
)
mcp_endpoint = result[ "mcpEndpoint" ] const res = await fetch ( 'https://api.weavz.io/api/v1/mcp/servers' , {
method: 'POST' ,
headers: {
'Authorization' : 'Bearer wvz_your_api_key' ,
'Content-Type' : 'application/json' ,
},
body: JSON . stringify ({
name: 'Agent for Alice' ,
workspaceId: workspace.id,
mode: 'TOOLS' ,
authMode: 'oauth_and_bearer' ,
endUserAccess: 'restricted' ,
}),
})
const { server , mcpEndpoint } = await res. json () res = httpx.post(
"https://api.weavz.io/api/v1/mcp/servers" ,
headers = headers,
json = {
"name" : "Agent for Alice" ,
"workspaceId" : workspace[ "id" ],
"mode" : "TOOLS" ,
"authMode" : "oauth_and_bearer" ,
"endUserAccess" : "restricted" ,
},
)
mcp_endpoint = res.json()[ "mcpEndpoint" ] Connect the client Give the AI agent the MCP endpoint. OAuth-capable clients can sign in through Weavz when the server uses OAuth. Provisioned clients can use an end-user bearer token.
Or issue a programmatic end-user token If you are provisioning MCP access from your own product, create a bearer token for a known end user:
curl TypeScript SDK Python SDK TypeScript Python
curl -X POST https://api.weavz.io/api/v1/mcp/servers/{serverId}/access-tokens \
-H "Authorization: Bearer wvz_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"endUserId": "user_123",
"scopes": ["mcp:tools"],
"expiresIn": 2592000
}' const { bearerToken , mcpEndpoint } = await client.mcpServers. createBearerToken (server.id, {
endUserId: 'user_123' ,
scopes: [ 'mcp:tools' ],
expiresIn: 60 * 60 * 24 * 30 ,
}) result = client.mcp_servers.create_bearer_token(
server[ "id" ],
end_user_id = "user_123" ,
scopes = [ "mcp:tools" ],
expires_in = 60 * 60 * 24 * 30 ,
)
bearer_token = result[ "bearerToken" ]
mcp_endpoint = result[ "mcpEndpoint" ] const res = await fetch ( `https://api.weavz.io/api/v1/mcp/servers/${ server . id }/access-tokens` , {
method: 'POST' ,
headers: {
'Authorization' : 'Bearer wvz_your_api_key' ,
'Content-Type' : 'application/json' ,
},
body: JSON . stringify ({
endUserId: 'user_123' ,
scopes: [ 'mcp:tools' ],
expiresIn: 60 * 60 * 24 * 30 ,
}),
})
const { bearerToken , mcpEndpoint } = await res. json () res = httpx.post(
f "https://api.weavz.io/api/v1/mcp/servers/ { server[ 'id' ] } /access-tokens" ,
headers = headers,
json = {
"endUserId" : "user_123" ,
"scopes" : [ "mcp:tools" ],
"expiresIn" : 60 * 60 * 24 * 30 ,
},
)
bearer_token = res.json()[ "bearerToken" ]
mcp_endpoint = res.json()[ "mcpEndpoint" ] Handle missing connections When a tool call fails because the end user hasn't connected the required integration, the error includes a setup URL. Share this URL with the end user so they can connect their account and the agent can retry.
Static MCP bearer tokens are still available for service-style clients that cannot complete OAuth, but they do not identify the connecting MCP client. Use MCP OAuth or API-created end-user bearer tokens when each client should resolve to a particular end user.
Best for strict isolation. Each customer gets their own workspace with separate integrations, connections, and end users.
graph TD
Org[Organization: My SaaS]
Org --> A[Workspace: Customer A]
Org --> B[Workspace: Customer B]
A --> Alice[alice - alice_123]
A --> Bob[bob - bob_456]
Alice --> AC[Alice's Slack]
Bob --> BC[Bob's Slack]
B --> Charlie[charlie - charlie_789]
Charlie --> CC[Charlie's Gmail]
Best for simpler setups where all customers share the same integration configuration.
graph TD
Org[Organization: My SaaS]
Org --> WS[Workspace: Production]
WS --> S[slack - per_user]
WS --> G[gmail - per_user_with_fallback]
WS --> Alice[alice]
WS --> Bob[bob]
WS --> Charlie[charlie]
Alice --> AS[Slack conn.]
Alice --> AG[Gmail conn.]
Bob --> BS[Slack conn.]
Bob -.- BG[Gmail fallback]
Charlie --> CG[Gmail conn.]
Charlie -.- CS[no Slack yet]
style BG stroke-dasharray: 5 5
style CS stroke-dasharray: 5 5
With per_user_with_fallback, end users who haven't connected their own account still get functionality through the workspace's default connection.
When you delete an end user, all their connections are automatically cleaned up:
curl TypeScript SDK Python SDK TypeScript Python
curl -X DELETE https://api.weavz.io/api/v1/end-users/{endUserUuid} \
-H "Authorization: Bearer wvz_your_api_key" await client.endUsers. delete (endUser.id) client.end_users.delete(end_user[ "id" ]) await fetch ( `https://api.weavz.io/api/v1/end-users/${ endUser . id }` , {
method: 'DELETE' ,
headers: { 'Authorization' : 'Bearer wvz_your_api_key' },
}) httpx.delete(
f "https://api.weavz.io/api/v1/end-users/ { end_user[ 'id' ] } " ,
headers = headers,
)
PreviousUsing Storage & KV Store Next Activity Logs