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.
Keep the ids, not the key
Section titled “Keep the ids, not the key”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.
| Id | Format | One per | Notes |
|---|---|---|---|
installId | ins_… | agent, user, and container | Stays the same when the same user reconnects your agent to the same container |
agentUserId | au_… | agent and user | Pairwise: 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.
Add a runtime key
Section titled “Add a runtime key”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:
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}.
Sign each request
Section titled “Sign each request”Every agent API request carries Authorization: Bearer <JWT>, a JWT your server signs with the runtime key for that one request.
| Part | Value |
|---|---|
| Header | alg: "EdDSA", kid: the runtime key’s id |
iss | Your agent id, for example someday |
aud | wire-agent-api (the older wire-app-api is still accepted) |
iat, exp | Seconds. exp at most 60 seconds after iat |
jti | A random id, at least 8 characters. Each is accepted once |
body_sha256 | POST, 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
Section titled “The agent API”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.
| Endpoint | What it does |
|---|---|
GET /installs/{installId} | One install |
GET /users/{agentUserId}/installs | Every 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}/export | Asks 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" } }.
The install shape
Section titled “The install shape”{ "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.statusisactivewhile any of the install’s connections is live, andrevokedotherwise. A revoked install stays readable and carriesreason:user_disconnected,agent_disconnected,uninstalled,container_deleted, orexpired. The old/api/v1/appspaths still answerapp_disconnectedfor the same case. A container in the trash reads as revoked withcontainer_deleteduntil it is restored.lastUsedAtis the last time the install’s credential was used, ornull.manageUrlopens 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, andupdateAvailablesay which version of your manifest the container runs and whether a newer one is waiting for approval.upgradeUrlappears only whileupdateAvailableistrue. See Updates.claimUrlappears 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.
Uninstalling from your server
Section titled “Uninstalling from your server”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.
Exporting an install’s container
Section titled “Exporting an install’s container”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: trueinstead of a new one. - Five a month. At five new archives in a calendar month (UTC, failed exports not counted), the request answers
429with aRetry-Afterheader and{ "success": false, "error": { "code": "EXPORT_LIMIT", "message", "retryAfter" } }, whereretryAfteris 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.
Install webhooks
Section titled “Install webhooks”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:
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.
Events
Section titled “Events”| Event | Sent when |
|---|---|
install.created | A connect made the install active: the first connect, or a reconnect after it was revoked |
install.upgraded | An 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.disconnected | The install’s last live connection ended: the user disconnected it, or you rotated your agent’s credentials |
install.uninstalled | Your agent was uninstalled from the container, by its owner or by your own DELETE |
install.claimed | The install’s trial container was claimed. install.agentUserId is now set |
install.expiring | About a day before a trial container expires, once per install |
install.expired | A trial container expired and is being deleted |
install.container_deleted | The container was permanently deleted |
After install.expired and install.container_deleted the install is gone, and reading it answers 404.
Payload
Section titled “Payload”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.
Verify the signature
Section titled “Verify the signature”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:
- The header is
alg: "EdDSA",typ: "wire-webhook+jwt", and akidfrom the key set. An action token (typ: "wire-action+jwt") is not a webhook. issiswire,audis your agent id, and the token is within its 60-second lifetime.wire_urlis the URL you registered, andwire_eventequals the body’sid.wire_body_sha256is 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.
Retries and duplicates
Section titled “Retries and duplicates”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.
Updates
Section titled “Updates”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.
| Field | Type | What it says |
|---|---|---|
installedVersion | string or null | The 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 |
latestVersion | string or null | The newest version you have registered. null when your agent has no manifest |
updateAvailable | boolean | true when the install is active and runs an older version than latestVersion |
upgradeUrl | string | Present 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.
Send people to upgradeUrl
Section titled “Send people to upgradeUrl”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.
Updates for a verified agent
Section titled “Updates for a verified 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 manifest | Asks |
|---|---|
| An action’s URL is on a host no installed action used | Yes, unless the host is under your verified domain |
A view’s csp list gains a domain it did not have in that same list | Yes, 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 reads | Yes, 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 before | Yes, 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 server | Yes |
base_tools gains a built-in tool | Yes |
builtin_tools turns a built-in on or off, on any transport, compared with the installed version | Yes |
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 app | Yes. 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-only | Yes |
A view’s permissions gains one | Yes |
analysis turns a graph on | Yes |
app.name changes | Yes |
| A new path, a new port, or a new action on a host already in use | No |
An action’s previous_urls gains or loses an address | No. 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 schema | No |
objects, instructions, skill, and a view’s html | No |
| Anything else removed, turned off, or lowered | No |
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.
Moving an action to a new address
Section titled “Moving an action to a new address”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.
-
Declare both. In the version that changes
url, list the old address in the action’sprevious_urls:{"name": "geocode","url": "https://new.example.com/geocode","previous_urls": ["https://old.example.com/geocode"]} -
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.
-
Watch. When your old endpoint has stopped receiving calls, nobody is left on the earlier version.
installedVersionon an install tells you which version it runs. -
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 equalurl. - Declare the old address in the same version that changes
url. Wire accepts a previous address only if the action had it, as itsurlor 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 theurlagain. - 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.
What your agent sees after an update
Section titled “What your agent sees after an update”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.
Trials
Section titled “Trials”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, withinstall.claimUrlin the payload.install.claimed, when they keep it. The payload’sinstall.agentUserIdis 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.