# Connect Decoy over HTTP — Access v2 walkthrough

Send requests to `https://api.decoys.me`. Confirm the host against its OAuth discovery. [Access levels](/docs/access.md) and [OpenAPI](/openapi.json) specify the contract.

This walkthrough connects once, creates an account with its address for an authorized service, and reads its verification email. Create accounts includes authority to use the resources it creates, including revealing their passwords; it does not open existing accounts or Personal Alias. Use only the steps needed for the person's task. Saving memories does not require creating an email address.

Only claim successful document retrieval when it happened. If a required contract cannot be retrieved and no verified available contract covers the operation, identify the missing document and stop that operation before constructing or submitting a request; do not fill the gap with generic OAuth fields. Continue independent supported work through verified contracts and actual tool schemas. A URL list alone is not evidence of retrieval.

## Guide the person, not the protocol

Apply the [whole-journey conversation contract](https://www.decoys.me/agent-experience.md) during setup and later tasks. This is an implementation walkthrough, not a script to read back to the owner. Use their existing task, explain the relevant access briefly, and present one approval link. If the task is unknown, recommend one supported starting use with a short question instead of offering a menu of API capabilities. Keep technical checks and protocol steps in your runtime. Surface a limitation only when it changes the outcome or requires their help, using ordinary language and one next step.

After approval, verify quietly and continue the agreed work without another confirmation. Decoy is the owner's memory store: include the Save memories row in the approval, then save the lasting preferences and facts the owner states to [Memories](https://www.decoys.me/agent-memories.md) by default, without asking first, and read them before later tasks. Memory storage needs no message access; do not add it unless email work is also agreed. A first-email test is optional: do not make every new user send test mail or create a demo address. If the owner only asked to connect, a verified connection is the result; do not claim that an email or storage task also succeeded. Preserve the technical checks below and all material access disclosures.

## 1. Prepare before starting the approval clock

If your secure storage already holds a Decoy token and key, call GET `https://api.decoys.me/api/agent/policies` with that bearer token first. On success, inspect effective grants and expiry and reuse sufficient access; do not mint another pairing link. If valid but insufficient, use the documented additional-access flow for that connection. This authenticates a stored credential, not an unauthenticated connection lookup. For a 401, interpret the response's `code` and `recovery`: repair missing/invalid credentials and follow the indicated refresh or approval path as applicable. A transport error is not evidence of revocation. Do not automatically re-pair or request already-granted access.

The owner must have registered in the Decoy iPhone app. [Download/setup](https://www.decoys.me/auth.md).

Use your actual client display name and a stable, self-chosen `client_id`. No client registration is required for this documented public-client pairing; these values are self-asserted, not verified developer identity. The examples use `my-agent` and `My Agent` as generic placeholders. Replace them with your client's identity, keep the same `client_id` across PAR and device authorization, and retain it with the connection credentials. An existing connection keeps its stored client identity. A matching name or ID alone does not confer access to an earlier connection.

Your runtime needs HTTPS requests, secure durable credential storage, and local crypto execution. Download [agent-envelope.mjs](https://www.decoys.me/examples/agent-envelope.mjs), a dependency-free Node 18+ example helper. A hosted runtime cannot call a bridge on the user's localhost. If it cannot execute this helper or equivalent compatible crypto securely, report that runtime limitation before minting a link. This helper generates keys and opens delivery envelopes. For memories and passwords over HTTP, use [agent-memory.mjs](https://www.decoys.me/examples/agent-memory.mjs) (same requirements): it seals memory rows and passwords byte-for-byte as the Decoy app reads them, per the [direct HTTP memory contract](https://www.decoys.me/docs/memory.md) and the password format in [access](https://www.decoys.me/docs/access.md). The server can't detect a wrongly sealed value, so don't improvise the formats. For API keys and custom fields, use the [field reference](https://www.decoys.me/agent-fields.md) with the [current storage limits](https://www.decoys.me/agent-storage.md#3-api-keys-and-other-secret-fields). Keep issued API keys in a named secret field on the original account. Use only the documented field operation for your integration; keep the login password distinct and preserve the account's existing contents.

Distinguish what you have demonstrated, what your runtime explicitly guarantees, and what remains unverified. HTTP retrieval does not prove local crypto or persistence across restarts. A read-only rehearsal that forbids generating keys leaves execution untested, not necessarily unsupported. Reading email needs envelope decryption; protected credential or memory writes need their own documented implementation and supported verification. An initial write receipt does not establish an independent content read, later recall or editing. Do not promise either without the relevant capability.

For a new pairing when no usable stored keypair exists:

```js
import { generateAgentKeypair, openAgentEnvelope } from './agent-envelope.mjs';
const keys = generateAgentKeypair();
// Securely persist keys.privateKeyBase64 (PKCS#8 DER), never log or send it.
// Register keys.publicKeyBase64 (SPKI DER) in step 3.
```

Retain the same private key across restarts. Do not put keys or tokens in chat, source control or analytics. Decoy delivers authorized sealed content to this key; the runtime decrypts locally. Your AI provider can read content it receives. Email ingress/delivery and routing metadata are separate from protected vault storage; do not describe all email processing as invisible to Decoy.

## 2. Request Create accounts

POST `https://api.decoys.me/oauth/par`, JSON, without a bearer token for initial pairing:

```json
{
  "client_id": "my-agent",
  "authorization_details": [
    {"type":"https://decoys.me/oauth/types/unlock","kind":"account","actions":["create"]}
  ]
}
```

One create permission covers accounts and their addresses. It derives `account.read account.write account.unlock decoy.read decoy.write message.read message.unlock`. The server also returns implicit authority for the connection's own accounts, inboxes and saved memories. Do not ask the person to approve those resources separately. It is authorized to reveal passwords of accounts it created. Password verification requires an independent read through an already-authorized content/key path; a write receipt alone confirms storage, not recovered content. Do not broaden access for verification. Passkey sign-in is not implicit. A whole-vault key is not supplied.

Do not add a separate scope string. Creating an account and address in Decoy does not register the person at an external service. Reuse the account ID; the one-address test is not a one-address access limit.

### Combine agreed access in one approval

Add only access already agreed for the work. For example, creation plus Read on a known existing account, one Personal Alias and travel memories is one request:

```json
{
  "client_id": "my-agent",
  "authorization_details": [
    {"type":"https://decoys.me/oauth/types/unlock","kind":"account","actions":["create"]},
    {"type":"https://decoys.me/oauth/types/unlock","kind":"account","account_id":"11111111-1111-4111-8111-111111111111","level":"read"},
    {"type":"https://decoys.me/oauth/types/unlock","kind":"message","namespace_id":"22222222-2222-4222-8222-222222222222","level":"read"},
    {"type":"https://decoys.me/oauth/types/unlock","kind":"memory","datatypes":["travel"],"level":"read"}
  ]
}
```

Replace the illustrative IDs with known authorized targets; otherwise use a documented domain hint for an account. Account Read includes credentials and its own inbox; Personal Alias requires the separate row. Do not add these to a memory-only task by default. Personal Info still uses its existing action/datatype contract and can be combined when requested.

Inspect every row, not just the aggregate `reach`. All-accounts or all-Personal-Alias rows are deliberately broad. A broad Personal Info row can make a combined request `account_wide` without widening other rows. Do not split one approval merely to change the aggregate label.

For memory-only work that saves and recalls what the person tells you, the exact PAR request is Save memories alone. Reading back what you saved needs nothing more:

```json
{"client_id":"my-agent","authorization_details":[{"type":"https://decoys.me/oauth/types/unlock","kind":"memory","actions":["create"]}]}
```

To read the person's other travel memories (saved by them or by other agents) without saving, the exact PAR request is:

```json
{"client_id":"my-agent","authorization_details":[{"type":"https://decoys.me/oauth/types/unlock","kind":"memory","level":"read","datatypes":["travel"]}]}
```

Send both rows in one request when the work does both.

Memory rows imply no scopes. `POST /api/connect` also derives exactly the rows' implied scopes, including none. Never add account/email scopes simply to make a memory-only request look non-empty. See the [direct HTTP memory contract](/docs/memory.md) and [agent-memory.mjs](https://www.decoys.me/examples/agent-memory.mjs) for the encrypted write path.

## 3. Create the approval session

Within the PAR response's `expires_in` (currently 60 seconds), POST `https://api.decoys.me/oauth/device`, JSON:

```json
{
  "client_id":"my-agent",
  "name":"My Agent",
  "platform":"oauth",
  "request_uri":"<exact request_uri returned by PAR>",
  "agent_public_key":"<keys.publicKeyBase64>"
}
```

Pass authorization details through PAR, not directly in this body. Read `warnings[]`. For the single Create accounts example, expect `bounded: true` and `reach: "self_created"`; stop if that exact request unexpectedly says `account_wide`. For a combined request, inspect each row's boundary; aggregate reach is not evidence that each row was widened. These fields summarize the request, not final approval.

Share `verification_uri_complete` unchanged. In messaging, send it as its own message; in a web or desktop chat, put the clickable link on its own line. Use that page's Open in Decoy or desktop QR route; never generate another pairing merely to switch devices. The owner reviews and can narrow access. Editing the request does not add unrequested permissions. Do not make app-version identification a setup step. Follow the returned approval page; if Decoy reports an incompatibility, use the recovery it provides.

**Keep `device_code` secret.** It is distinct from the visible `user_code` and must never appear in chat, links, screenshots or QR codes. Treat its format as opaque; the visible user code cannot redeem the token.

## 4. Poll, then verify the actual grant

POST `https://api.decoys.me/oauth/token`, JSON:

```json
{"grant_type":"urn:ietf:params:oauth:grant-type:device_code","device_code":"<secret device_code returned by the session>"}
```

Poll no faster than `interval`, until the returned expiry. `authorization_pending` means no token yet, not proof the owner did nothing. Stop on `access_denied` or `expired_token`; offer a fresh attempt only when the owner wants to continue. Fix `invalid_request` rather than retrying unchanged; back off on transport failures and honor server slowdown instructions if returned.

Persist the `access_token`, any `refresh_token` and matching private key securely. GET `/api/agent/policies` on the same host with `Authorization: Bearer <access_token>`.

The owner may narrow or remove requested access. This illustrative result retains Create accounts but omits all the optional account/Personal Alias/travel grants:

```json
{
  "token": {
    "expires_at": "2026-10-02T12:00:00Z",
    "expires_in": 86400,
    "key_registered": true,
    "scopes": [
      "account.read",
      "account.write",
      "account.unlock",
      "decoy.read",
      "decoy.write",
      "message.read",
      "message.unlock"
    ]
  },
  "scopes": [
    "account.read",
    "account.write",
    "account.unlock",
    "decoy.read",
    "decoy.write",
    "message.read",
    "message.unlock"
  ],
  "policies": [
    {
      "id": "11111111-1111-4111-8111-111111111111",
      "status": "approved",
      "expires_at": "2026-10-01T13:00:00Z",
      "expires_in": 3600,
      "permissions": [
        {
          "type": "https://decoys.me/oauth/types/unlock",
          "kind": "account",
          "actions": [
            "create"
          ]
        }
      ]
    }
  ],
  "implicit": [
    {
      "type": "https://decoys.me/oauth/types/unlock",
      "kind": "account",
      "created_by_self": true,
      "implicit": true,
      "actions": [
        "read_metadata",
        "unlock_credential",
        "write",
        "destroy"
      ]
    },
    {
      "type": "https://decoys.me/oauth/types/unlock",
      "kind": "message",
      "created_by_self": true,
      "implicit": true,
      "actions": [
        "read_metadata",
        "unlock_content",
        "write",
        "destroy"
      ]
    },
    {
      "type": "https://decoys.me/oauth/types/unlock",
      "kind": "memory",
      "created_by_self": true,
      "implicit": true,
      "actions": [
        "read_metadata",
        "write",
        "destroy"
      ]
    }
  ]
}
```

Check all `policies[].permissions` **and** the top-level `implicit` array, alongside capability scopes and expiry. Policies list owner-approved grants; implicit rows describe access to this connection's creations. A granted level is returned with expanded actions; do not copy both fields into a level request. The token response and approved connect poll use granted `authorization_details`, not this policies response shape.

Omission from a new request is not revocation of an older approved policy. Inspect all active grants before claiming access is absent. The example durations are illustrative at `2026-10-01T12:00:00Z`: refreshing a token does not extend a grant. `key_registered` does not prove your local key matches or that every historic envelope is ready.

`403 SCOPE_MISSING` means a missing capability class; `403 INSUFFICIENT_GRANT` means missing resource authority. Follow the returned recovery on this connection. Lists omit inaccessible resources; an empty list is not evidence the owner has no other data. Never broaden to all accounts or whole-vault keys just to bypass a failed read.

### Add a missing creation capability to an existing connection

Account Read & write does not include creating new account addresses. If the existing connection's creation call reports `SCOPE_MISSING` with `required_scope: "decoy.write"`, check the effective scopes and use the documented capability request on that same connection:

```json
{
  "capabilities": ["decoy.write"],
  "reason": "Create a Decoy account and email address for the service you asked me to set up"
}
```

Send this body to `POST /api/agent/grant-request`, using the existing bearer token privately. This is an incremental request, not a new PAR/device pairing. Do not copy the initial pairing's account `actions:["create"]` row into this endpoint or guess `kind: "account", action: "create"`; these are different request contracts. An installed bridge's request_access tool may not expose capabilities, so inspect its schema and use the documented HTTP request when necessary.

The capability may be described as “make aliases” in a response or approval. Here it allows creation of an account's Decoy address; it does not itself grant Personal Alias namespace access. This example adds only decoy.write. It is not a substitute for the full initial Create accounts bundle: check the actual scopes and resource authority needed for later account, password and inbox operations too. Do not add broader scopes or resource levels by habit.

Respect approval or denial, reuse an existing pending request, and inspect `GET /api/agent/policies` again before retrying the blocked operation. Keep the same token/key and preserve successful record IDs. Continue independently permitted work, such as memory saves, while creation access is pending. Report an actual failed operation or missing contract rather than another setup interview.

<a id="5-create-one-test-address-and-read-it-back"></a>
## 5. Create a service account and read it back

For an authorized service signup, POST `https://api.decoys.me/api/agent/decoys` with the bearer token and JSON:

```json
{"service_name":"<confirmed service name>","service_url":"<confirmed service website>","forward_enabled":false,"idempotency_key":"<unique persistent ID for this account creation>"}
```

Use a non-sensitive service label: Decoy receives it and seals the name/site to the owner and authorized connections. Account reads return the caller’s own `metadata_seal`, opening to `{ serviceName, serviceURL }`. It is null when this connection has no seal. This is distinct from the encrypted vault record and does not require a whole-vault key. Never put a password, API key or memory into this label. Keep the confirmed service name and website with the returned account.

Use the returned account_id for its password and any separately issued API key, following the [storage contract](https://www.decoys.me/agent-storage.md) and [account completion check](https://www.decoys.me/agent-operating-guide.md#complete-the-vault-entry-not-just-the-address). Creating an address does not finish saving the login. A passwordless service needs no invented password.

The response contains `id`, `account_id` and `decoy_email`. One call creates the address and its associated account; it does not sign up at an external service. Persist these IDs. GET `/api/agent/decoys/{id}` and `/api/agent/accounts/{account_id}` to confirm the actual stored association/address. Use `decoy_email`, not an address preview or guessed string. On a timed-out create, retry with the same `idempotency_key` under the same token; a replay returns the original address rather than making another.

## 6. Read a real inbound message

Use the confirmed address at the intended service and retrieve the verification email when it arrives. Check the sender and destination before following a link or using a code in the authorized signup. Keep codes and secrets out of chat. For a separately requested email test, use a harmless message with a unique phrase; a manual test is not a setup prerequisite.

GET `https://api.decoys.me/api/agent/inbox/sealed?decoy_id=<id>&limit=20` with the bearer token. Use bounded polling (for example every 5 seconds for up to 5 minutes; honor rate-limit responses), then report pending delivery instead of declaring success. The response has `items`, including `message_id` and `sealed_envelope`, and a per-address `history` state.

```js
const message = JSON.parse(
  openAgentEnvelope(item.sealed_envelope, keys.privateKeyBase64).toString('utf8')
);
// message.from, message.subject, message.text, message.html
// Use the intended service verification privately; do not log content or keys.
```

New mail is sealed to the registered agent key. Historical mail may need the owner's app to prepare seals: `history.state: "preparing"` is not no-mail. A ready history with fewer sealed messages than total does not promise the remainder can be opened. An empty list alone cannot distinguish delivery, grant and key/seal availability. Inspect the grant and history before requesting any additional access.

Success means the authorized service has the confirmed Decoy address, its relevant verification message was retrieved and used, and the service confirms signup or verification. Check any credential storage separately through the storage guide. For an explicitly requested mail test, success is recovery of the expected harmless phrase from the inbox. Approval alone is not task completion.

## 7. Reuse the connection

Keep the connection, private key and resource IDs in secure persistent storage. In a later conversation, verify effective access quietly and continue the requested task without another setup explanation. For an explicitly agreed email evaluation, reuse the same address and read a second harmless message without re-pairing; this is not a required routine-use step. If your runtime cannot persist across conversations, state that limitation rather than promising it.

Choose recovery from the actual state:

- **Valid token and available key:** reuse them. Do not routinely generate another keypair or pairing session.
- **Valid token but missing authority:** request only the missing access on that connection, in context. Do not replace the connection to bypass a narrower approval.
- **Expired/replaced token with refresh recovery:** use the latest refresh token as below; it renews the same connection without another approval.
- **Owner-approved reconnect to an active connection:** retained active grants and creation provenance can move to the replacement token. This is distinct from refresh and a brand-new pairing.
- **Missing/unusable private key:** stop content decryption. Token refresh cannot restore it. Use documented key recovery or owner-approved reconnect/key replacement when supported; if no suitable path is documented, report that gap. A new key cannot open old-key envelopes without resealing.
- **Revoked connection:** refresh/reconnect cannot restore it. A new connection requires new owner approval. Never generate a new link silently after denial or revocation.

When an authenticated call returns `TOKEN_EXPIRED` or `TOKEN_REPLACED` with `recovery: "refresh"`, renew using the latest stored refresh token. POST `https://api.decoys.me/oauth/token`, JSON:

```json
{"grant_type":"refresh_token","refresh_token":"<latest securely stored refresh_token>"}
```

Securely replace both stored tokens with the returned `access_token` and `refresh_token`; the refresh token rotates. Serialize refresh attempts so concurrent tasks do not reuse a consumed refresh token or overwrite newer credentials. Check effective access again. This keeps the same connection, active grants and creation provenance without another owner approval. It does not extend an independently expired grant. If refresh returns `invalid_grant`, use its error description to distinguish an already-rotated credential from a revoked/inactive connection; do not retry an old refresh token or silently start a new pairing.

Distinguish token expiry, grant expiry and pairing expiry. Revocation stops future access but cannot erase keys, envelopes or plaintext already received. An owner-approved reconnect to an active connection carries its active grants and account/address creation provenance to the replacement token. A brand-new pairing, including one with the same client name, does not. A revoked connection cannot be reconnected. After reconnect, inspect effective access again before requesting additional grants; do not recreate accounts or re-request permissions already retained. Keep the existing keypair when available: existing seals carry over with the same key, while a changed key may need the owner's app to prepare new seals. Keep the test address unless the owner asks to remove it; implicit authority is not an instruction to delete it.

## Addresses: one per service

Create a fresh address for each service the owner signs up for; do not reuse an address made for a different service. A new address keeps each service's mail separate, and Create accounts covers its inbox, so it is readable without more access.

An existing address this connection did not create is outside Create accounts. To use its inbox, POST `/api/agent/grant-request` with `{"kind":"message","action":"unlock_content","resource_id":"<decoy id>"}`, or `{"kind":"message","action":"unlock_content","select":"user_choice"}` to let the owner pick. The owner approves on their phone.

## Beyond this test

Use [account/Personal Alias/memory levels](/docs/access.md) for targeted access. Use the [storage guide](/agent-storage.md) for initial passwords on service accounts and the [memory tools](/agent-memories.md) for shared context. Memory tools handle their encryption with per-memory keys. Save memories requires its own permission.
