Skip to content

Connecting an organization: the path ​

This page is the end-to-end route a partner takes from "we want our users to have an agent" to a working chat with tools. It names every screen, key and call in the order they are used. The detailed contracts live on the neighbouring pages: provisioning, server chat, identity.

The reference integration is Store Deputy: one Store Deputy team is one Agentfy team, one connected store is one agent, and the store's catalog is an MCP server. Inline provisioning was also verified with an isolated local MCP fixture; see the task verification report.

Who is who ​

TermMeaning
OrganizationThe partner inside Agentfy. Has an immutable namespace and environment (test or production), operators, credentials and an approved policy.
OperatorA direct Agentfy user who manages the organization: owner, admin, developer, billing, support. Operators never see customer teams or conversations.
Platform administratorAgentfy staff. Activates organizations and reviews organization policy changes that widen code execution, internet access, trusted context or team limits in the admin panel.
External actorA user of the partner's product. Agentfy knows it only by externalUserId inside the namespace; it has no password and no direct login.
TeamAn Agentfy team created for the partner's workspace (team.externalId). Agents, conversations and bills belong to it.
ProfileWhat an agent is: name, instructions, skill, model, optional MCP server and optional worker tools. Sent with each request and frozen into its operation.
OperationOne provisioning request and its state machine: pending → ready, or failed, or retired.
MCP serverThe partner's own HTTP endpoint that exposes tools to the agent. Agentfy dials it with a per-agent credential.
Partner product                Partner backend                     Agentfy
───────────────                ───────────────                     ───────
user clicks "create agent"  →  POST /provisionings (provisioning key)  →  operation, team, agent
                               PUT  …/configuration {mcp credential}   →  tools/list on your MCP
                                                                       ←  ready
user opens chat             →  POST /chat/conversations (chat:* key)
                               PUT  …/context {grant}                   →  trusted MCP context
                               POST …/messages                         →  run, SSE events
                                                                       →  tool calls to your MCP

Step 1. Create the organization ​

In the app, Organizations (/orgs) → Create organization. The creator becomes the owner and confirms that responsibility on the form.

  • namespace and environment are permanent. Use a separate organization for test and for production; they never share teams, credentials or profiles.
  • Free. Billing is not connected yet. No contract, contract budget, card or redirect URI is required to create, activate or provision an organization. Usage does not create debt or future payment obligations. Resource limits, approved policies, operator permissions and platform suspension still apply. Future paid usage requires separate opt-in; free-period usage must never be billed retroactively.
  • Add operators on the same screen. Operator membership is independent of customer teams and grants no platform role.

The organization starts as active, with an approved version 1 policy and 100 agent slots. Issue a server key and connect an agent immediately; no platform activation or initial policy submission is needed. Existing pending organizations still require platform activation. Suspended or closed organizations cannot issue keys or connect agents.

Policy and requests for more agents ​

The starter policy allows all currently configured platform models and does not restrict MCP domains. Trusted context, dangerous worker tools (*, exec, process_exec) and unrestricted worker internet are not granted. Runtime resource limits, model entitlement and network address checks still apply.

In Organizations → your organization → Policy, the owner sees Agents: N of 100. Select Request more, enter the desired total and an optional comment, then send. The request copies the current policy and changes only its limit. It appears in the existing admin policy queue. The old limit remains usable during review; approval replaces it, while rejection shows the platform's reason. Existing drafts must be finished first. Each live agent reservation consumes a slot, including additional agents in an existing team. Historical team reservations remain a floor on the count, preserving existing resource limits. The 101st new team is refused with 409 PROVISIONING_LIMIT until a higher limit is approved.

In Organizations → your organization → Policy, an owner or admin lists the allowed models, MCP hosts, trusted context, worker tools, worker destinations and maximum teams, then saves and submits the draft. Most submissions take effect immediately: adding MCP hosts, models, worker tools other than exec/process_exec, specific destinations, or narrowing anything. A submission goes to platform review only when it widens a reviewed grant relative to the policy in force: code execution tools (exec, process_exec, all tools), public internet access for workers, trusted context, or a team limit above the platform threshold (POLICY_AUTO_APPROVE_MAX_TEAMS, default 100). The form says which it will be before you submit. For reviewed submissions a platform administrator opens the policy queue under Organizations in the admin panel, compares the diff and approves or rejects with a reason. Only an approved policy permits provisioning; a pending replacement leaves the previous version effective.

Step 2. Issue server credentials ​

Organizations → Integration · Server credentials. Create one key and select the permissions needed by your server. Provisioning (provision:create, provision:read) and chat (chat:membership, chat:create, chat:context, chat:run, chat:read) may be combined. Separate keys remain useful to limit individual services. Chat/identity permissions require an owner or platform administrator and explicit approval; admin/developer operators may issue provisioning-only keys.

Secrets are shown once and stay on your server. Keys default to No expiry; an optional future date has no 90-day ceiling. Existing dates are preserved. Rotation preserves permissions and expiry; the old key ends immediately or after the chosen overlap (at most 24 hours, never later than its existing expiry). Revocation works at any time. Organization, namespace, environment and status restrictions still apply.

Identity permissions are reserved; no working identity issuance/management API is currently implemented. The legacy kind field is optional compatibility metadata and grants no rights.

Step 3. Describe the agent inline ​

The organization's policy is approved once (automatically for the default envelope, by the platform for widened grants). Send agent.profile in each provisioning request: {name, instructions, skill: {name, description, body}, modelId?, mcp?: {url, requiredTools, trustedContext?}, worker?: {tools, egress?}}. Content changes within policy need no platform approval. Omit mcp for a conversation-only agent.

requiredTools is the minimum that must be present, not an allowlist: every discovered tool is available, subject to the usual caller permissions and tool-name collision rules. Every successful PUT …/profile refreshes an unchanged MCP connection after the profile is saved; connection changes still use the setup worker. Changing only requiredTools also refreshes without suspending a working agent.

For all attached MCP servers (including manually connected servers), the first eligible turn after MCP_TOOLS_REFRESH_SECONDS (default 300 seconds) starts a background refresh. That turn keeps its saved tool list; new tools become available on the next turn after refresh completes. Idle agents do not poll. A failed refresh is retried after the same interval; an explicit profile save or Refresh bypasses the age check. Discovery uses a 5-second timeout per HTTP exchange in the background, so it adds no network wait to the triggering turn.

Refresh rejects metadata containing the supplied credentials or a list missing any required tool. The last successful list and discovery timestamp remain intact; lastError is shown in the agent's MCP settings and cleared after a successful refresh. This client does not keep a connection open between requests, so notifications/tools/list_changed is not subscribed to; profile saves and age-based refresh provide updates.

The model must belong to allowedModels; the MCP host must pass the public-address floor and, when mcpHostsRestricted is enabled, fit mcpHosts; trusted context requires trustedContextAllowed; worker tools and egress must fit workerTools and workerEgress. The deployment egress ceiling still applies. Name ≤120, instructions ≤16,000, skill name ≤64, description ≤1,024, body ≤32,000 characters; required MCP tools 1–64, worker tools 1–16, egress entries at most 64.

agent.profile is required. Omit modelId to use the first model in the approved policy. Removed templateId and templateVersion fields return 422, as do other unknown request fields. There is no saved content catalogue or default selection.

In the Policy tab, All tools stores workerTools: ["*"]: every current and future runtime worker tool is allowed by the policy. The per-tool checklist is disabled while checked. An agent still requests its own explicit tools; network destinations and the deployment ceiling remain separate limits.

Admin → Organizations → Provisioned agents shows the read-only operation history: external ID, team, subject, status, creation time and expandable frozen profile. Existing snapshots and approved policies are unchanged. Live-agent content updates are tracked separately in AGNT2-477.

Step 4. Expose your MCP server ​

If the agent should do anything with your data, you host an MCP server over HTTP and put its address into the profile.

  • The address must be public HTTPS. Agentfy refuses loopback, private networks and .internal/.local names before anything else (the address floor), and resolves the name to check every address behind it. Locally, MCP_DEV_LOOPBACK=true with NODE_ENV=dev admits localhost over plain HTTP.
  • Authentication: Agentfy sends Authorization: Bearer <credential> on every call. The credential is the one you hand over in step 5, so you mint it, you verify it.
  • Discovery: during provisioning Agentfy calls initialize, notifications/initialized and tools/list; every name in requiredTools must be present or the operation fails with mcp_tools_missing.
  • Trusted context (trustedContext: true): on every tool call Agentfy adds X-Agentfy-Agent-Id, X-Agentfy-Thread-Id, X-Agentfy-Turn-Id, X-Agentfy-Tool-Call-Id, X-Agentfy-Initiated-By: human and the conversation grant you bound in step 6 as X-SD-Context. Your server uses them to decide what this agent may read for this person right now.
  • A turn ending with done.stopReason: completed is completed, even after failed tool steps. Only a turn ending on tool_error fails with TOOL_REFUSED; max_iterations remains limited. Use run_status or run polling as outcome authority. Both include toolErrors: [{toolCallId, name, errorCode}], empty when no tool failed. errorCode is the safe TOOL_FAILED, never raw arguments or results. A strict client may treat any nonempty toolErrors as unsuccessful.

Step 5. Provision: a team and its agent ​

All calls use the provisioning key and Idempotency-Key; retries with the same key and body return the same operation.

First team of a workspace (bootstrap):

http
POST /v1/orgs/{orgId}/provisionings
Authorization: Bearer <provisioning key>
Idempotency-Key: workspace-77-v1

{
  "subject": { "externalUserId": "person-42" },
  "team":    { "externalId": "workspace-77", "name": "Workspace 77" },
  "agent": {
  "externalId": "store-9",
  "profile": {
    "name": "Store assistant",
    "instructions": "Help the authorized user with the store.",
    "skill": {
      "name": "store-help",
      "description": "Store assistance",
      "body": "Use the attached MCP for authorized store requests."
    },
    "modelId": "<allowed model>",
    "mcp": {
      "url": "https://mcp.example.com/mcp",
      "requiredTools": [
        "store_describe"
      ]
    }
  }
},
  "payer":   { "mode": "organization" }
}

subject becomes the team owner. The answer (202 while pending) gives operationId, userId (the external actor's Agentfy id), teamId, agentId, status, step, reason, retryable, version and configurationVersion. Store the operation, team and agent ids: they are the only handles you have.

Another agent in an existing team: POST /v1/orgs/{orgId}/teams/{teamId}/agents/provisionings with subject (must already be an owner or admin of that team, see memberships in step 6) and agent.

Configure the MCP credential. With an MCP profile the operation parks at pending / configuration_required. Mint the credential your MCP server will accept and hand it over:

http
PUT /v1/orgs/{orgId}/operations/{operationId}/configuration
{ "expectedVersion": 1, "mcp": { "credential": "<secret your MCP verifies>" } }

Agentfy stores it encrypted as the agent's secret and never returns it. Rotating later is the same call with the current expectedVersion.

Wait for readiness. Poll GET /v1/orgs/{orgId}/operations/{operationId}. Steps go profile → mcp_discovery → complete; status: ready means the agent is active with its skill, MCP server and discovered tools.

reasonWhat it meansRetryable
configuration_requiredwaiting for the credential from the PUT aboveafter PUT
policy_violation: <field>the profile exceeds the approved organization policy (or no policy is approved)widen and approve the policy, then retry the same operation with its current expectedVersion
model_unavailablethe profile's model is not in the catalogue or above the planafter fixing the platform settings
egress_refusedthe MCP address is inside a private network or outside the platform MCP host policyno, fix the profile
mcp_discovery_failedyour MCP did not answer initialize/tools/listPOST …/retry { "expectedVersion" }
mcp_tools_missingrequiredTools not all present in tools/listafter fixing your server
setup_profile_missingoperation created before profiles existedno

Admission violations return HTTP 422 with code: policy_violation and message: policy_violation: <field> before allocation. Fields include policy, modelId, mcpHosts, mcp.url, trustedContextAllowed, workerTools, workerEgress. Correct the profile or obtain an approved policy change and resubmit. Model catalogue/plan entitlement is still checked during setup.

The same inline content is accepted by the additional-agent endpoint. Omit team and payer there. Without MCP, no configuration call or discovery is needed. With MCP, configure the credential below and wait for authenticated discovery.

The accepted content is frozen in the operation snapshot and request fingerprint. Replays keep that snapshot after policy changes. A changed request under the same key returns IDEMPOTENCY_CONFLICT; changed intent for a live external reservation returns EXTERNAL_ID_CONFLICT. Content changes to a live agent belong to the separate in-place update workflow, not retirement. Retirement is for intentional removal/replacement.

Step 6. Chat through your backend ​

Chat requires the specific chat:* scopes for each operation; it may use the same key as provisioning. Your product's UI talks to your backend; your backend talks to Agentfy. Browser code never sees the key or a grant.

  1. Sync membership whenever a user's role in your workspace changes: PUT /v1/orgs/{orgId}/chat/memberships with externalUserId, externalTeamId, role, active, sourceRevision, validUntil. Revoked members lose chat and provisioning rights immediately.
  2. Open a conversation per user and agent: POST /v1/orgs/{orgId}/chat/conversations {externalUserId, externalTeamId, agentId}. The answer includes mcp: {serverId, destination} when the agent has an MCP server.
  3. Bind the trusted context before the first message (and again before it expires): mint a short-lived grant for this person and conversation on your side, then PUT …/conversations/{id}/context with serverId, destination (must equal what the conversation returned), grant, expiresAt (at most five minutes ahead) and expectedVersion. Agentfy stores the grant encrypted and presents it to your MCP as X-SD-Context. GET …/context reads version and expiry without the grant.
  4. Send a message: POST …/conversations/{id}/messages {externalUserId, externalTeamId, message} with an Idempotency-Key → 202 {runId, status: accepted}.
  5. Stream the run: GET …/runs/{runId}/events is SSE with turn, text (deltas), tool_call, tool_result {ok}, done and the authoritative run_status {status, reason, toolErrors, timings}. Last-Event-ID resumes. GET …/runs/{runId} is the same truth without streaming; GET …/messages is the durable history. thinking is private and is not emitted. Event names and required JSON fields are listed in the run event contract. Omit Last-Event-ID or use {runId}:0 to replay the complete run; {runId}:N resumes after N, including after completion.

A turn ending with done.stopReason: completed is completed, even after failed tool steps. Only a turn ending on tool_error fails with TOOL_REFUSED; max_iterations remains limited. Use run_status or run polling as outcome authority. Both include toolErrors: [{toolCallId, name, errorCode}], empty when no tool failed. errorCode is the safe TOOL_FAILED, never raw arguments or results. A strict client may treat any nonempty toolErrors as unsuccessful.

Step 7. Give the agent hands: worker tools ​

With worker.tools in the policy-compliant profile, Agentfy starts an isolated worker pod for the agent on demand and offers the listed tools (exec runs shell commands in the workspace, file reads and writes it, http and web_fetch reach the network, browser* drive a browser). The worker stops after idleSeconds without calls.

Network tools are bounded twice: by the installation ceiling (RUNTIME_WORKER_EGRESS_ALLOWLIST) and by the agent's own request. Set worker.egress explicitly within the organization policy; omission means no network access for http and web_fetch.

The organization policy also limits worker destinations; requested destinations must fit inside both that policy and RUNTIME_WORKER_EGRESS_ALLOWLIST. Policy approval does not bypass public-address checks. A narrower policy gates new or retried provisioning; it does not retroactively revoke already-ready agents.

Step 8. Update content in place or retire ​

Update content in place ​

Use the provisioning key and the current version from GET …/operations/{operationId}. Send the complete inline profile, with the same fields and policy validation as step 5. Omitted optional fields are removed; omitted modelId uses the first allowed model.

http
PUT /v1/orgs/{orgId}/operations/{operationId}/profile
Authorization: Bearer <provisioning-key>
Content-Type: application/json

{
  "expectedVersion": 3,
  "profile": {
    "name": "Store assistant",
    "instructions": "Use the revised store procedures.",
    "skill": { "name": "store-guide", "description": "Store procedures", "body": "Follow the revised guide." }
  }
}

Instructions, name, skill, model and worker grant change on the same agent. Content-only changes stay ready and do not run discovery. A changed MCP URL or required-tools set makes the operation pending; discovery reuses the stored credential, then returns ready or failed with the existing retryable reason. Adding MCP without a credential waits for configuration. After fixing the remote endpoint, POST …/retry with the latest expectedVersion; a failed operation must recover before another profile PUT. Removing MCP detaches it.

After an MCP URL change, wait for the operation to return to ready, then re-bind every existing conversation before its next message. Context grants are keyed by (serverId, destination) and are not transferred to a new URL. With the chat:* key and the same actor fields, GET …/conversations/{id}/context for the new destination’s current version (0 if never bound), then PUT the same endpoint with expectedVersion from GET, the unchanged serverId, the new canonical destination, a fresh grant authorized for that destination, and expiresAt. Do not reuse the old destination’s version. Follow step 6 for the context request fields.

A stale version returns 409 OPERATION_VERSION_CONFLICT; a policy violation returns 422. Only ready operations accept profile updates. Profile updates require provision:create, even when the same key also has chat scopes. Agent id, team, subject and conversations remain unchanged; a running turn retains its loaded instructions and the next turn reads the new content. Original POST bodies still replay against the original fingerprint. Admin → organization → Provisioned agents shows each accepted revision, timestamp and submitting key.

Retire ​

For intentional removal or a change of externalId, team or subject, retire and re-provision:

http
DELETE /v1/orgs/{orgId}/teams/{teamId}/agents/{externalAgentId}
Authorization: Bearer <provisioning key>

204 archives the agent, revokes its secrets and MCP attachment, closes its conversations and tombstones the reservation; the old operation reads retired. The same externalId can then be provisioned again and gets a new agent, a new operation and the profile you choose. The call is idempotent; unknown ids answer the opaque 404. Treat a 404 as success only after GET …/operations/{id} confirms retired.

Environments and local development ​

  • Each credential belongs to one environment. The API's PARTNER_API_ENVIRONMENT (test or production) must match; it is independent of NODE_ENV.
  • Local Agentfy with MCP_DEV_LOOPBACK=true in api/.env.dev accepts http://localhost:… MCP addresses in profiles, discovery and chat context. Nothing changes outside NODE_ENV=dev.
  • A public tunnel to a laptop (serveo, cloudflared) works for a demo against a hosted Agentfy, but each new address can be sent inline if its host is admitted by the organization policy. Prefer the loopback switch for development.

What your backend has to keep ​

ValueWhere it comes from
organization id, environment, key(s) with provisioning and chat permissionsstep 2, server config
profile sent with the requeststep 3
per workspace: externalTeamId, Agentfy teamIdstep 5 bootstrap answer
per agent: externalAgentId, operationId, agentId, your MCP credential, configurationVersionstep 5
per user: externalUserId, membership sourceRevisionstep 6
per conversation: conversationId, serverId, destination, current grant and context versionstep 6
per message: idempotency key and runIdstep 6

Store Deputy requires STOR-59 before it can send the inline contract. Its integration stores these records in OrganizationWorkspace, OrganizationProvision, OrganizationChatMember and OrganizationConversation; its agent/orgProvision and agent/orgChat slices describe the partner integration path; deploy STOR-59 immediately after this API change.

MCP domain restriction is optional: new policies in the editor send mcpHostsRestricted: false, so a public MCP URL in the agent profile needs no organization host entry. Enable Allow only specified MCP domains in advanced policy settings to enforce mcpHosts; an enabled empty list blocks every MCP connection. Existing policies and legacy API requests omitting the flag stay restricted, including empty lists. Changing the mode takes effect only after submission/approval; all other approval gates, URL validation, private-address protection and platform/worker network limits remain in force.

Organization cost analytics and local walkthrough ​

Issue analytics:read in the organization's server-key form (owner/admin or platform admin; developers cannot grant financial access). It may share a provisioning key, or use a separate analytics key. GET /v1/orgs/{orgId}/analytics?period=7d|30d|month accepts this key as Bearer and returns the same aggregation as the owner/admin JWT report. Keys are restricted to their own organization and the deployment's matching environment: another organization is 404, missing scope is 403, expired/revoked or wrong-environment keys are 401. JWT access is unchanged. /analytics/live and /analytics/year remain JWT-only.

priceSide: "cost" means recorded provider cost, not resale charges. costUsdPico is an exact decimal integer string (10⁻¹² USD); unpricedEventsExcluded: true means unpriced events are excluded from that sum, never zero-priced. Inspect unpricedEvents beside every total. Run usage: null is unpriced, not free.

GET /v1/orgs/{orgId}/credentials/current inspects the authenticated key's organization, environment and scopes without returning the secret or granting human authority. It works for any valid active organization key, including one missing analytics scope.

The dependency-free local /organization demo has five inspectable steps and ru/en text: connection/scope diagnostics, saved team, agent provisioning, conversation with per-turn cost and structured parts, then analytics and session reconciliation. See demo/README.md in the repository. Server keys stay in the local server configuration.