end users.md
End users
An end user is one of your users, kept under the id you already hold for them. Their agents,
spend caps, delegated tokens and costs all hang off that one record, and the
Users page in the dashboard shows the same list. Everything here is a
method on maritime.endUsers (TypeScript) or client.end_users (Python).
Create or update
upsert is keyed on externalId (1 to 128 characters of letters, digits and _ @ . : + -). The first
call creates the end user, later calls update only the fields you pass, so it is safe to run on every
sign-in. metadata holds flat JSON values you want to see next to the user later; tags (at most
eight) are for filtering.
TypeScript
const endUser = await maritime.endUsers.upsert(`user_${user.id}`, {
displayName: user.name,
metadata: { plan: user.plan, region: 'eu' },
tags: [user.plan],
})
Python
end_user = client.end_users.upsert(
f"user_{user.id}",
display_name=user.name,
metadata={"plan": user.plan, "region": "eu"},
tags=[user.plan],
)
Give them an agent
createAgent creates the end user's agent exactly once: the first call creates it (and the end user,
if you skipped upsert), a repeat call returns the same agent. It takes the same options as
agents.provision; leave name out and the external
id names the agent. agents lists everything the end user owns, in the full agent shape.
TypeScript
const agent = await maritime.endUsers.createAgent(`user_${user.id}`, {
template: 'openclaw',
instructions: "You are this customer's assistant.",
})
const owned = await maritime.endUsers.agents(`user_${user.id}`) // [agent]
Python
agent = client.end_users.create_agent(
f"user_{user.id}",
template="openclaw",
instructions="You are this customer's assistant.",
)
owned = client.end_users.agents(f"user_{user.id}") # [agent]
Every agent call also accepts an X-Maritime-End-User header carrying an external id. With it,
GET /api/agents lists only that end user's agents and any other agent answers 404, which keeps a
request made on behalf of one user from ever touching another's.
Policy: caps and sizing
A policy caps what one end user can spend and sets how their agents are sized. setPolicy replaces
the whole policy and applies it to every agent the end user owns now (new agents get it on create).
clearPolicy removes it. The caps apply to each of the end user's agents. When an agent hits the AI cap its LLM
calls stop and the agent.llm_limit_reached webhook fires.
| Field | Caps or sets |
|---|---|
llm_spend_cap_cents_per_month |
AI spend this end user may use per calendar month, in cents |
compute_minutes_cap |
awake minutes per month across their agents |
wakes_per_hour |
how often their agents may be woken |
idle_sleep_seconds |
how long an agent stays awake with nothing to do |
mem_mb |
RAM for each of their agents |
vcpus |
CPU cores for each of their agents |
disk_gb |
persistent disk for each of their agents |
TypeScript
const { appliedToAgents } = await maritime.endUsers.setPolicy(`user_${user.id}`, {
llmSpendCapCentsPerMonth: 500, // $5 of AI per month
wakesPerHour: 60,
idleSleepSeconds: 600,
})
await maritime.endUsers.getPolicy(`user_${user.id}`) // { policy, appliedToAgents }
await maritime.endUsers.clearPolicy(`user_${user.id}`) // account defaults again
Python
result = client.end_users.set_policy(f"user_{user.id}", {
"llm_spend_cap_cents_per_month": 500, # $5 of AI per month
"wakes_per_hour": 60,
"idle_sleep_seconds": 600,
})
client.end_users.get_policy(f"user_{user.id}") // {"policy": {...}, "appliedToAgents": n}
client.end_users.clear_policy(f"user_{user.id}") // back to your account defaults
Delegated tokens for their browser
Your mk_ key never leaves your server. When the end user's own browser or app should talk to their
agent directly, mint a short-lived eu_ token from your backend and hand it over. It opens that one
agent only, with the scopes you choose, until it expires (30 to 3600 seconds, default 900).
revokeTokens cuts every token the end user holds at once.
| Scope | Opens |
|---|---|
chat |
POST /chat on the agent |
files |
the files routes (list, download, upload, write, move, delete) |
console |
the terminal and desktop websockets, and POST /exec |
logs |
GET /logs and the logs websocket |
app |
the agent's own web UI under /ui |
TypeScript
// your backend, on page load:
const { token, agentId, expiresAt } = await maritime.endUsers.mintToken(`user_${user.id}`, {
scopes: ['chat', 'files'],
ttlSeconds: 900,
})
// the end user's browser, with only the eu_ token:
await fetch(`https://api.maritime.sh/api/agents/${agentId}/chat`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ message: 'hello' }),
})
// websockets take it as ?token=eu_...
Python
# your backend, on page load:
minted = client.end_users.mint_token(
f"user_{user.id}", scopes=["chat", "files"], ttl_seconds=900
)
# hand minted["token"] and minted["agentId"] to the browser; it calls
# POST https://api.maritime.sh/api/agents/{agentId}/chat with
# Authorization: Bearer eu_... (websockets take ?token=eu_...)
client.end_users.revoke_tokens(f"user_{user.id}") # sign them out everywhere
Suspend, resume, delete
suspend puts the end user's agents to sleep and refuses every wake, message and spend until
resume; use it for an unpaid invoice or an abuse report. delete removes the end user with their
agents, tokens and policy; it returns at once and the cleanup finishes in the background. Each step
fires a webhook (end_user.suspended, end_user.resumed, end_user.deleted).
TypeScript
await maritime.endUsers.suspend(`user_${user.id}`) // status: 'suspended'
await maritime.endUsers.resume(`user_${user.id}`) // status: 'active'
await maritime.endUsers.delete(`user_${user.id}`) // agents and tokens go with it
Python
client.end_users.suspend(f"user_{user.id}") # status: "suspended"
client.end_users.resume(f"user_{user.id}") # status: "active"
client.end_users.delete(f"user_{user.id}") # agents and tokens go with it
Webhooks know the end user
Every webhook delivery about an end user's agent carries an
end_user object (its Maritime id, a UUID, and your external id) next to the agent, so your receiver
routes on your own id without a lookup. Events about an agent you own yourself carry
end_user: null.
{
"event": "agent.sleeping",
"agent_id": "a1b2c3",
"external_id": "user_42",
"end_user": { "id": "7f3a2c1e-5b9d-4e8a-9c41-2d6f0b8e1a37", "external_id": "user_42" }
}
Usage and rebilling
The usage report lists every agent with its awake minutes, hosting cost and AI cost over a range,
and each row names its externalUserId. Group on it and you have a per-user invoice. Under a seat
plan the hosting cost is your plan price allocated over your agents day by day
(hostingBasis: "allocated"); AI cost is always the real spend from your credits. The daily series
gives the same numbers by day; the summary at the top of the Users page draws both.
TypeScript
const report = await maritime.billing.usage({ from: '2026-09-01', to: '2026-10-01' })
const byUser = new Map<string, number>()
for (const row of report.agents) {
const key = row.externalUserId ?? 'you'
byUser.set(key, (byUser.get(key) ?? 0) + row.totalCostCents)
}
const daily = await maritime.billing.usageDaily({ from: '2026-09-01', to: '2026-10-01' })
Python
report = client.billing.usage(from_="2026-09-01", to="2026-10-01")
by_user: dict[str, float] = {}
for row in report["agents"]:
key = row["externalUserId"] or "you"
by_user[key] = by_user.get(key, 0) + row["totalCostCents"]
daily = client.billing.usage_daily(from_="2026-09-01", to="2026-10-01")
From the CLI
The same operations are one command away for support work and scripts, under maritime end-users (alias
users). Commands that send a body take it as wire JSON with --body-json, or from stdin with --stdin.
Every command takes --json.
maritime end-users list --limit 50
maritime end-users get user_42
maritime end-users upsert user_42 --display-name "Ada" --tags pro,eu
maritime end-users create-agent user_42 --body-json '{"templateId":"openclaw"}'
maritime end-users agents user_42
maritime end-users set-policy user_42 --body-json '{"policy":{"llm_spend_cap_cents_per_month":500,"wakes_per_hour":60}}'
maritime end-users policy user_42
maritime end-users clear-policy user_42
maritime end-users mint-token user_42 --body-json '{"scopes":["chat","files"],"ttlSeconds":900}'
maritime end-users revoke-token user_42
maritime end-users suspend user_42
maritime end-users resume user_42
maritime end-users delete user_42
maritime usage get --from 2026-09-01 --to 2026-10-01
maritime usage daily --from 2026-09-01 --to 2026-10-01