Skip to content

Installs and webhooks

An agent that brings a manifest can show “connected to Places, manage it in Wire” on a return visit without storing anyone’s API key. When a user connects, your agent gets two ids, installId and agentUserId. Keep those. Later, your server asks Wire about them with a runtime key that only your agent holds, and Wire can also tell your server when an install changes.

This page covers the ids, the runtime key, the agent API endpoints, install webhooks, how your users get a new version of your manifest, and how trials (ephemeral containers) show up in both.

Every connection result includes installId and agentUserId. Neither grants access to anything, so they are safe to store in your own database next to your user record.

IdFormatOne perNotes
installIdins_…agent, user, and containerStays the same when the same user reconnects your agent to the same container
agentUserIdau_…agent and userPairwise: another agent gets a different id for the same person. It never reveals the user’s Wire account. null while the user is on a trial with no Wire account, and set when they claim the container

The device flow’s poll (GET /api/v1/sdk/poll) returns them as install_id and agent_user_id, and so does the connection object of the browser flow’s token response. GET /api/v1/sdk/status returns them under connection. Responses still carry the older app_user_id beside agent_user_id, with the same value.

A typical pattern: store agentUserId on your user when the connect completes, then list that user’s installs from your server whenever you render a settings or manage page. Store installId too if your agent deals with one container per user.

The agent API authenticates your server with an Ed25519 key registered for your agent with purpose: "runtime". It is a separate key from the publisher key that registers your manifest, and neither is accepted in place of the other: the key a live server holds can read, export, and uninstall your agent’s own installs and nothing else.

An org member who can update your agent’s owner organization adds it:

Terminal window
curl -X POST https://app.usewire.io/api/v1/agents/someday/publisher-keys \
-H 'content-type: application/json' --cookie "$WIRE_SESSION" \
-d '{ "publicKey": "<Ed25519 public key, raw 32 bytes, base64url>", "label": "production server", "purpose": "runtime" }'

The response names the key’s id (pk_…). Keep the private half as a server secret. Revoke a key with DELETE /api/v1/agents/{agentId}/publisher-keys/{keyId}.

Every agent API request carries Authorization: Bearer <JWT>, a JWT your server signs with the runtime key for that one request.

PartValue
Headeralg: "EdDSA", kid: the runtime key’s id
issYour agent id, for example someday
audwire-agent-api (the older wire-app-api is still accepted)
iat, expSeconds. exp at most 60 seconds after iat
jtiA random id, at least 8 characters. Each is accepted once
body_sha256POST, PUT, PATCH, and DELETE: base64url (no padding) SHA-256 of the exact request body, which for an empty body is 47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU and for {} is RBNvo1WzZ4oRRq0W9-hknpT7T8If536DEMBg9hyq_4o. Not sent on GET

A token signed with a publish key, for another audience, with a reused jti, naming another agent in iss, or with a body_sha256 that does not match the body is refused with 401.

The agent API has five endpoints, all under https://app.usewire.io/api/v1/agents/{agentId}, and each only ever sees your own agent’s installs. A path naming another agent, another agent’s installId, or an agentUserId your agent does not know all answer 404, never 403.

EndpointWhat it does
GET /installs/{installId}One install
GET /users/{agentUserId}/installsEvery install that user has with your agent, newest first, as { "installs": [ … ] }
DELETE /installs/{installId}Uninstalls your agent from that install’s container, then returns the install as it now reads
POST /installs/{installId}/exportAsks Wire to export the install’s container for the person who connected it. See Exporting an install’s container
GET /installs/{installId}/exports/{exportId}The status of that export

Responses use the same envelope as the rest of the API: { "success": true, "data": … }, or { "success": false, "error": { "code", "message" } }.

{
"installId": "ins_4f0c9k2m1q8r7t6v5w3x2y1z",
"agentUserId": "au_9a8b7c6d5e4f3g2h1j0k9m8n",
"container": {
"id": "b7d7c7f0-…",
"name": "Places",
"mcpEndpoint": "https://jit.mcp.usewire.io/container/b7d7c7f0-…/mcp",
"orgSlug": "jit",
"isEphemeral": false,
"ephemeralExpiresAt": null
},
"claimed": true,
"connection": {
"status": "active",
"connectedAt": "2026-09-27T18:04:28.000Z",
"lastUsedAt": "2026-09-27T19:12:03.000Z"
},
"installedVersion": "0.3.0",
"latestVersion": "0.3.0",
"updateAvailable": false,
"manageUrl": "https://app.usewire.io/containers/b7d7c7f0-…/connections#installed-agents"
}
  • connection.status is active while any of the install’s connections is live, and revoked otherwise. A revoked install stays readable and carries reason: user_disconnected, agent_disconnected, uninstalled, container_deleted, or expired. The old /api/v1/apps paths still answer app_disconnected for the same case. A container in the trash reads as revoked with container_deleted until it is restored.
  • lastUsedAt is the last time the install’s credential was used, or null.
  • manageUrl opens the container’s installed agents in the Wire dashboard, where the user can disconnect or uninstall your agent. Link to it from your own manage screen.
  • installedVersion, latestVersion, and updateAvailable say which version of your manifest the container runs and whether a newer one is waiting for approval. upgradeUrl appears only while updateAvailable is true. See Updates.
  • claimUrl appears only while the install is active and its container is an unclaimed trial. See Trials.

The install never includes an API key, the container’s contents, the user’s Wire account id, or their email.

DELETE /installs/{installId} does exactly what the container owner’s Uninstall button does. Your agent’s connections to that container end, the container is released (Wire’s built-in tools and the analysis graphs go back to their defaults, and the owner’s settings unlock), and the container’s data stays. The container’s activity feed records the uninstall as your agent’s. The response is the install, now revoked with reason uninstalled.

Uninstalling is per container, so it also ends other users’ connections of your agent to that same container. A 502 means the connections were ended but the container could not finish the uninstall yet; send the same request again.

Disconnecting without uninstalling stays a dashboard action.

Your agent can ask Wire to export the container of one of its installs, so a person can leave with their data from your own screens. The export is the same one the container’s owner gets from the dashboard: an archive of their files, records, links, and settings.

POST /installs/{installId}/export with the body {}. The request is signed like any other, with body_sha256 set to the hash of {}.

{
"success": true,
"data": {
"exportId": "V1StGXR8_Z5jdHi6B-myT",
"status": "queued",
"createdAt": "2026-10-02T15:04:05.000Z",
"reused": false
}
}

The response is 202. Wire builds the archive in the background, then emails the person who connected your agent a link to download it. Your agent never gets the archive or the link, and downloading it needs that person’s Wire sign-in. The export runs as that person, so it covers what they can see in the container.

Read its progress with GET /installs/{installId}/exports/{exportId}:

{
"success": true,
"data": {
"exportId": "V1StGXR8_Z5jdHi6B-myT",
"status": "completed",
"createdAt": "2026-10-02T15:04:05.000Z",
"completedAt": "2026-10-02T15:04:41.000Z",
"expiresAt": "2026-10-09T15:04:41.000Z"
}
}

status is queued, running, completed, failed, or expired. The download link works until expiresAt, 7 days after the export completes. A completed export means the email has gone out.

The same limits apply as in the dashboard, per container and whoever asks:

  • One new archive a day. If the container’s last export started less than 24 hours ago, you get that export back with reused: true instead of a new one.
  • Five a month. At five new archives in a calendar month (UTC, failed exports not counted), the request answers 429 with a Retry-After header and { "success": false, "error": { "code": "EXPORT_LIMIT", "message", "retryAfter" } }, where retryAfter is when the container can be exported again.

The install has to be active: an install whose connections have all ended answers 409 with AGENT_DISCONNECTED. A trial container answers 409 with INSTALL_IS_TRIAL, because there is no account to send the link to until the person claims it. If the person who connected your agent can no longer see the container, the answer is 403. An export id that is not one of this install’s container’s exports answers 404.

Wire can POST a signed event to your server whenever one of your installs changes, so your records stay right between visits. Register the URL and the events on your agent record:

Terminal window
curl -X PATCH https://app.usewire.io/api/v1/agents/someday \
-H 'content-type: application/json' --cookie "$WIRE_SESSION" \
-d '{ "webhooks": { "url": "https://someday.example/webhooks/wire", "events": ["install.created", "install.uninstalled", "install.disconnected", "install.container_deleted", "install.claimed", "install.expiring"] } }'

The URL follows the same rules as an action URL: https only, no credentials in it, no private or loopback address, and not a Wire domain. Send "webhooks": null to stop them. Changing webhooks needs the same permission as adding a key.

EventSent when
install.createdA connect made the install active: the first connect, or a reconnect after it was revoked
install.upgradedAn active install moved to a newer version of your manifest: someone approved it, through your connect flow or in the Wire dashboard, or your agent is verified and the update was applied on its own
install.disconnectedThe install’s last live connection ended: the user disconnected it, or you rotated your agent’s credentials
install.uninstalledYour agent was uninstalled from the container, by its owner or by your own DELETE
install.claimedThe install’s trial container was claimed. install.agentUserId is now set
install.expiringAbout a day before a trial container expires, once per install
install.expiredA trial container expired and is being deleted
install.container_deletedThe container was permanently deleted

After install.expired and install.container_deleted the install is gone, and reading it answers 404.

The body is JSON with four fields. install is the same shape the agent API returns, including the version fields, as it was when the event happened; for install.expired and install.container_deleted its container name and mcpEndpoint are null, and so is installedVersion. For now it also carries appUserId, the deprecated name for agentUserId, with the same value. Read agentUserId.

{
"id": "evt_5b1e0c2a9f8d7e6c5b4a39281706f5e4",
"type": "install.uninstalled",
"createdAt": "2026-09-27T20:11:52.000Z",
"install": { "installId": "ins_…", "connection": { "status": "revoked", "reason": "uninstalled", "…": "…" }, "…": "…" }
}

The request also carries X-Wire-Event-Id and X-Wire-Event-Type, and User-Agent: Wire-Webhooks/1.

Each request carries Authorization: Bearer <JWT>, signed by Wire with the same published keys as action calls: https://app.usewire.io/.well-known/wire-actions-jwks.json. Verify it before you act on the body:

  1. The header is alg: "EdDSA", typ: "wire-webhook+jwt", and a kid from the key set. An action token (typ: "wire-action+jwt") is not a webhook.
  2. iss is wire, aud is your agent id, and the token is within its 60-second lifetime.
  3. wire_url is the URL you registered, and wire_event equals the body’s id.
  4. wire_body_sha256 is the base64url (no padding) SHA-256 of the raw body bytes you received. Hash before you parse.

The SDK’s verifyWireWebhook (in @usewire/sdk/app from 0.9.0) does all of this for you, and from 0.10.0 it reads either field name.

Delivery is at-least-once, so your handler must treat a repeated event id as already handled. Answer any 2xx once you have the event; the response body is ignored.

Anything else is retried for about 24 hours: a non-2xx status, a redirect (redirects are never followed), a timeout (8 seconds), or a network error. Each round is one attempt and one quick retry, and the waits between rounds grow from 1 minute to 8 hours. After the last round the event is dropped. Answer 410 Gone to stop retries of an event at once.

An event keeps its id on every retry, and if Wire notices the same change twice (a retried disconnect, say) it sends it once, under the same id.

Registering a new version of your manifest changes no container by itself. For an agent that isn’t verified, each install keeps the version its user approved until a person approves the new one, either through your connect flow or in the Wire dashboard. For a verified agent, an update that needs no new permissions is applied the next time the person uses your agent, and one that needs more waits for their approval (see Updates for a verified agent). Every install you read, and every install in a webhook, says which version it runs and whether a newer one is waiting, so your agent can offer the update on its own screens.

FieldTypeWhat it says
installedVersionstring or nullThe version of your manifest the install’s container runs. null when your agent has no manifest, the install is gone, or Wire can’t tell
latestVersionstring or nullThe newest version you have registered. null when your agent has no manifest
updateAvailablebooleantrue when the install is active and runs an older version than latestVersion
upgradeUrlstringPresent only when updateAvailable is true. Opens the review screen for this install in the Wire dashboard

An install that is behind reads like this:

{
"installId": "ins_4f0c9k2m1q8r7t6v5w3x2y1z",
"agentUserId": "au_9a8b7c6d5e4f3g2h1j0k9m8n",
"container": { "id": "b7d7c7f0-…", "name": "Places", "…": "…" },
"claimed": true,
"connection": { "status": "active", "connectedAt": "2026-09-27T18:04:28.000Z", "lastUsedAt": "2026-09-28T09:40:17.000Z" },
"installedVersion": "0.1.0",
"latestVersion": "0.3.0",
"updateAvailable": true,
"manageUrl": "https://app.usewire.io/containers/b7d7c7f0-…/connections#installed-agents",
"upgradeUrl": "https://app.usewire.io/containers/b7d7c7f0-…/connections?upgrade=someday#installed-agents"
}

A disconnected or uninstalled install never reads updateAvailable: true. Connecting your agent again installs the version you have registered, once the person approves it on the connect screen.

upgradeUrl works like manageUrl: it opens the container in the Wire dashboard, signing the person in first if they need to, and goes straight to the review for your agent. There they see what the new version installs and what it changes from the version they have, and they approve it or leave it for later. Nothing about the approval happens on your site. Who can approve is the same as for your connect flow; see Updating an agent.

On a return visit, a line like this is enough, with the last part linking to upgradeUrl:

Connected to Places · Someday 0.1.0 · Update to 0.3.0

Show the update link only while updateAvailable is true, and read the install again rather than caching the answer: the update can be approved in the Wire dashboard without anyone visiting your agent.

A verified agent’s update is applied without a review when it asks for nothing its user hasn’t already agreed to. It is applied the next time the person uses your agent: when a client they connected through your agent’s URL uses the container, usually within about ten minutes, or when they connect another client to it. It is applied as that person, and the container’s activity feed records it as automatic. An install nobody is using keeps its version: nothing is pushed to a container that isn’t in use. An install reached only with a key your own software holds is updated when the person connects again, or approves the update in the Wire dashboard.

The update waits for approval when the new version does any of the following, compared with the version the container has installed:

Change in the new manifestAsks
An action’s URL is on a host no installed action usedYes, unless the host is under your verified domain
A view’s csp list gains a domain it did not have in that same listYes, unless the domain is under your verified domain
Container records can leave for the first time: your first tool with an after action that receives the base tool’s output, or your first view with any csp domain while a tool of yours readsYes, always, including under your verified domain
A tool’s after action, or any csp domain of a view, can receive container records on a host that received none beforeYes, unless the host is under your verified domain
An after action is given more of the base tool’s output than any action on that host was before (a new {{tool.…}} path)Yes
Your enabled tools and base_tools together gain a capability: read, SQL read, write, delete, calling your server, your server setting a tool’s arguments, or sending records to your serverYes
base_tools gains a built-in toolYes
builtin_tools turns a built-in on or off, on any transport, compared with the installed versionYes
A tool that writes, deletes, or sends records to your server becomes callable from a view: you ship your first ui entry, or the tool’s ui.visibility gains appYes. Mark such a tool visibility: ["model"] if a view should not call it
A tool’s annotations stop marking it destructive or open-world, or start marking it read-onlyYes
A view’s permissions gains oneYes
analysis turns a graph onYes
app.name changesYes
A new path, a new port, or a new action on a host already in useNo
An action’s previous_urls gains or loses an addressNo. A previous address is not somewhere the new version sends anything
Tools added, changed, or removed within the capabilities you already had, including a tool’s SQL, description, and schemaNo
objects, instructions, skill, and a view’s htmlNo
Anything else removed, turned off, or loweredNo

Hosts are compared, not URLs. Your verified domain is the registrable domain of your agent’s own hostname: an agent served on mcp.example.com is trusted on example.com and every name under it. On a shared hosting domain, where each customer has its own name, only your own name and the names under it are trusted. An agent with no hostname of its own has no verified domain.

A manifest Wire can’t compare with the installed one is treated as asking. While an update waits, the install reads updateAvailable: true with an upgradeUrl, exactly as for an unverified agent, so the same “Update to 0.3.0” link applies.

Keep permission changes and everything else in separate versions when you can. A version that only fixes a tool reaches a user the next time they use your agent. One that also adds a host waits for each of them.

An install calls the url of the manifest version it has installed, and keeps that version until the person updates. Wire signs a call only to an address your current manifest declares for that action. So a version that changes an action’s url and nothing else breaks that tool for every install still on the earlier version, until each person updates.

To move without breaking anyone, treat the address like a key you are rotating: declare both, then retire the old one.

  1. Declare both. In the version that changes url, list the old address in the action’s previous_urls:

    {
    "name": "geocode",
    "url": "https://new.example.com/geocode",
    "previous_urls": ["https://old.example.com/geocode"]
    }
  2. Keep serving both. Installs on the earlier version go on calling the old address, and Wire goes on signing for it. Installs that update call the new one.

  3. Watch. When your old endpoint has stopped receiving calls, nobody is left on the earlier version. installedVersion on an install tells you which version it runs.

  4. Retire. Register a version without the old address. From then on Wire refuses to sign a call to it. Do this before you give up the old host, so an address you no longer control is never one Wire signs for.

The rules:

  • An action can keep up to three previous addresses. Each is an https URL under the same rules as url, and none can equal url.
  • Declare the old address in the same version that changes url. Wire accepts a previous address only if the action had it, as its url or as a previous address, in the version being replaced. A first version can’t declare one, and an address you retired can’t be declared again. If you need it back, make it the url again.
  • Addresses are kept per action name. A renamed or removed action is not covered: installs on the earlier version fail it. If you want them to keep working, keep the action declared until you retire it.
  • Wire never calls a previous address for an install on the version that declares it. It isn’t shown on the connect screen and isn’t something a person is asked to approve. Moving to a host people haven’t approved is still an update they are asked about, as in the table above.

If you move or remove an action without declaring its old address, the registration still succeeds and its answer carries a warning with the code ACTION_ADDRESS_DROPPED, naming the actions. registerManifest() returns it in warnings from SDK 0.18.0. To go back, register a version that declares the action at its old address again, with the new one in previous_urls so installs that already updated keep working, then move again with both declared.

Your agent keeps its connection. It doesn’t reconnect or get a new key, and from the moment the update is applied its access follows the new version’s tools. You get install.upgraded, whether the update was approved through your connect flow or in the dashboard or applied on its own, and the install then reads the new installedVersion with updateAvailable: false. The container’s activity feed records the update.

SDK 0.13.0 exposes installedVersion, latestVersion, updateAvailable, and upgradeUrl on WireInstall.

A trial is a connect by someone with no Wire account: they get an ephemeral container that lasts 7 days, with the same tools and endpoint. Its install has an installId right away and agentUserId: null, and reads container.isEphemeral: true with container.ephemeralExpiresAt and claimed: false.

While the trial is active, the install also carries claimUrl, a link where the person creates an account and keeps the container. The link works until the trial expires, so it can go in an email. It stops working if your agent is disconnected from or uninstalled from the container first.

An agent that offers trials usually subscribes to two events:

  • install.expiring, about a day before the container goes, to remind the person, with install.claimUrl in the payload.
  • install.claimed, when they keep it. The payload’s install.agentUserId is now set: store it on your user, as you would after a normal connect.

If they don’t claim it, install.expired follows, and the container and its data are deleted.