# Headless HTTP interoperability

Agent Site's primary marketplace interface is HTTPS, JSON, OpenAPI and OAuth. Start at `/openapi.json` (OpenAPI 3.1.1) or `/api` for public capabilities. No SDK, MCP, HTML parsing, hypermedia runtime or browser automation is required. The domain is independent of HTTP; browser pages are optional adapters. Private state is never made public by discovery.

Public documentation: [browser guide](https://agent-site.tatemz.com/docs), [this Markdown guide](https://agent-site.tatemz.com/docs/interoperability.md), [OpenAPI](https://agent-site.tatemz.com/openapi.json), and [assistant instructions](https://agent-site.tatemz.com/llms.txt). None requires sign-in, a special user-agent or JavaScript.

A client needs external HTTP or browser access, a way to transfer the person's attachment, and a way to send the person through authorization. A chat platform lacking those capabilities cannot gain them from this website. MCP, a marketplace SDK and a dedicated plugin are optional.

| Client capability                | Available experience                                                                                      |
| -------------------------------- | --------------------------------------------------------------------------------------------------------- |
| Construct and share a URL        | Prepare the link below without an HTTP request; the person completes the browser flow.                    |
| Operate a browser                | Use ordinary links, forms and file inputs with JavaScript disabled.                                       |
| Make HTTP requests               | Read public schemas; authenticated clients can prepare JSON drafts and upload photo bytes.                |
| HTTP and delegated authorization | Create a draft, upload bytes, request exact owner approval, publish through JSON and retrieve its canonical URL. |

A URL-only client must know or be given the documented URL format. Agent Site cannot make a web-reading tool fetch a domain that the tool refuses to access.

## Prepare and publish

1. Discover the authorization server at `/.well-known/oauth-authorization-server` and the draft resource at `/.well-known/oauth-protected-resource/api`.
2. Register a public OAuth client at `POST /oauth/register` with an exact HTTPS redirect URI (HTTP loopback is allowed for local clients), `token_endpoint_auth_method: "none"`, `grant_types: ["authorization_code"]`, and `response_types: ["code"]`. Client registration does not grant user access. Request `refresh_token` in `grant_types` only when refresh access is needed.
3. Open `/oauth/authorize` in the person's browser with `response_type=code`, the issued `client_id`, exact `redirect_uri`, `resource=https://agent-site.tatemz.com/api`, unpredictable `state`, `code_challenge_method=S256`, the SHA-256/base64url challenge, and scopes `drafts:read drafts:write media:write listings:publish`. The last scope is opt-in; omitting scope grants preparation only. The person signs in and explicitly allows access. Verify `state` on return; exchange the one-use code with the original `code_verifier` at `POST /oauth/token` using standard URL-encoded fields. The person never copies credentials or tokens into a chat. A trusted owner authorization UI may request the consent document with `Accept: application/json`; the agent does not receive the owner's cookie or password.
4. `POST /api/drafts` with `Authorization: Bearer ...`, `Content-Type: application/json` and a unique `Idempotency-Key`. Draft fields can be supplied incrementally. Example:

   ```json
   {
     "title": "LaCroix Sparkling Water Can",
     "description": "Selling one LaCroix sparkling water can. Cash only. Local meetup at the hotel lobby.",
     "amountMinor": 1,
     "currency": "USD",
     "pickupLocation": "Hyatt Mission Bay Resort hotel lobby, San Diego, CA",
     "paymentMethods": ["cash"],
     "category": "beverages"
   }
   ```

5. `PATCH /api/drafts/{id}` supplies changed fields. `POST /api/drafts/{id}/photos` accepts `multipart/form-data` with one `file`. Both operations require `Idempotency-Key` and the current `If-Match: "vN"` ETag. Use JPEG, PNG or WebP, at most 10 MiB and 20 million decoded pixels; at most 10 photos per draft. The server decodes and rotates the image, strips metadata, normalizes it to WebP and stores encrypted bytes. It does not fetch external image URLs. Operation IDs in OpenAPI identify these operations; returned links are optional conveniences.
6. `GET /api/drafts/{id}/validation` returns `{draftId, version, valid, issues}` without changing state. Fix incomplete fields before requesting approval.
7. `POST /api/drafts/{id}/publication-authorizations` with JSON `{}`, current `If-Match` and a unique `Idempotency-Key` returns 201 with `{id, draftId, draftVersion, actionHash, status: "pending", expiresAt, authorizationUrl}`. This freezes the exact fields and photo hashes for this OAuth grant. It neither approves nor publishes. Pending requests expire after ten minutes; at most twenty live requests per grant.
8. Send the person `authorizationUrl`. They review the exact snapshot and photographs, confirm explicitly and reenter their password. This only approves the requested action; it does not execute publication. Alternatively, a trusted owner UI can use `GET /api/publication-authorizations/{id}/review` and `POST /api/publication-authorizations/{id}/approve` with owner session, CSRF, reviewToken, password and `consent: "approve"`. These owner-only credentials never enter the agent's context. The server holds the proof. Approval expires within two minutes and any field/photo edit invalidates it.
9. Poll `GET /api/publication-authorizations/{id}` with the agent bearer token. Status is `pending`, `approved`, `published`, `cancelled`, `expired` or `invalidated`. When approved, `POST /api/drafts/{id}/publish` with JSON `{"authorizationId":"publication_..."}`, matching `If-Match` and a new `Idempotency-Key`. The transaction verifies the live OAuth grant and approving owner session, consumes the exact-action proof and publishes atomically. The 200 JSON result is `{listing, canonicalUrl, receipt}`; Location is the canonical URL. A different key cannot reuse a consumed proof. `DELETE /api/publication-authorizations/{id}` cancels an unconsumed request idempotently.
10. Verify with `GET /api/listings/{id}`. `/api/listings/{id}/receipt` returns the same signed public confirmation; `/listings/{id}` is an optional public HTML representation. The receipt identifies the broker signing key and explicitly reports `independentlyWitnessed: false`. It is not evidence of payment, delivery, identity or an independent checkpoint. Pin the broker signing key through a trusted channel before relying on its signature.

The private repository includes the native HTTP example `scripts/http-listing.ts`. Run `bun scripts/http-listing.ts --photo /path/to/can.jpg` to exercise this LaCroix scenario against the public service using your own account and explicit approval. It discovers operation URLs from OpenAPI, uses standard fetch and a loopback OAuth callback, keeps tokens in process memory, and never parses HTML. This creates a real public listing only after you approve. The matching isolated integration test is `test/headless-publication.test.ts`.

`GET /api/listings?q=LaCroix&category=beverages&limit=20` searches active public snapshots. `q` is a case-insensitive title/description substring (max 200 characters); category is an exact filter (max 100). Limit is 1–100, default 20. Continue using `after=nextCursor` with unchanged filters; null means the final page. Results exclude legacy private records and owner identifiers.

All unsafe API operations require the appropriate OAuth scope. A write-only scope does not imply arbitrary draft reads. Each authorization can access only drafts created through that authorization; the owner can review all their drafts. `listings:publish` alone never substitutes for exact-action owner approval and grants no deal authority. Access tokens last five minutes; grants last seven days. Optional refresh tokens rotate; reuse revokes the affected grant. Revoke an assistant from `/account`. A new OAuth authorization is a distinct draft boundary; resume using the original rotating refresh token rather than authorizing again to recover its drafts.

`DELETE /api/drafts/{id}` discards a private draft with version and idempotency checks. Published listings are immutable in this workflow; editing, withdrawing and deleting a public listing through these new interfaces are not implemented. Existing MCP listing operations do not expose their legacy records through the new public listing endpoints.

## Browser forms and restricted clients

All preparation, upload, review, publication, sign-in and revocation operations have ordinary HTML forms. They require no custom JavaScript. Sign-in uses a unique username and an Argon2id-protected password, with an eight-hour HttpOnly, SameSite=Lax session cookie (Secure on HTTPS). There is no password recovery yet. These new accounts have only draft preparation authority; they cannot replace passkey verification in the existing deal workflow.

A restricted assistant can generate this prefilled URL:

```text
https://agent-site.tatemz.com/listings/new?title=One%20LaCroix%20sparkling%20water%20can&description=One%20LaCroix%20sparkling%20water%20can.%20Cash%20only.%20In-person%20pickup.&price=0.01&currency=USD&pickupLocation=Hyatt%20Mission%20Bay%20Resort%20hotel%20lobby%2C%20San%20Diego%2C%20California&paymentMethod=cash
```

All accepted prefilled fields are:

| Field            | Accepted value                                                                    | Alias      |
| ---------------- | --------------------------------------------------------------------------------- | ---------- |
| `title`          | Up to 200 characters                                                              | —          |
| `description`    | Up to 4,000 characters                                                            | —          |
| `price`          | Decimal USD; at most ten integer digits and two fractional digits, such as `0.01` | —          |
| `currency`       | `USD` only, the default                                                           | —          |
| `pickupLocation` | Public pickup location, up to 4,000 characters                                    | `location` |
| `paymentMethod`  | `cash`, `bank_transfer`, or `other`; defaults to `cash`                           | `payment`  |
| `category`       | Optional, up to 100 characters; defaults to `general`                             | —          |

URL-encode values. All fields are optional for preparation, but review requires a complete title, description, price, pickup location and payment method. Fulfillment is in-person pickup in this HTTP flow; no separate fulfillment query parameter is needed. There is no image query parameter. Unknown parameters, including `utm_source`, are ignored. Duplicate recognized fields, conflicting canonical/alias values, unsupported currency and invalid price/payment values return 400. Keep the handoff path within 12,000 characters; browser and chat URL limits can be lower. Enter longer text in the browser after sign-in.

`price` uses decimal USD; JSON uses integer `amountMinor`. Opening the URL only renders an editable form. Anonymous edits are preserved when its sign-in button is submitted; this does not create a draft. After authentication, the person saves a draft, selects any attachment the client could not transfer, reviews and confirms publication. If a photograph is only attached to a chat, save/download it to the device and choose that file on the draft page. The marketplace cannot retrieve private chat attachments. Never put passwords, bearer tokens, photos, private instructions or secret prices in URLs. Query strings can remain in browser history and infrastructure logs.

Browser forms use POST for saves, uploads, discard, consent, publication, sign-out and revocation. JSON clients use POST, PATCH and DELETE as described by OpenAPI. GET never publishes, changes listing terms, discards or revokes. OAuth consent GET can issue an expiring authorization-request ticket; it grants no access until POST consent. Review GET issues a signed review document without authorizing publication.

Cookie-authenticated API writes require `X-CSRF-Token` from an authenticated `GET /api` and the matching CSRF cookie. Forms carry their own hidden token. Foreign Origin headers and cross-site cookie writes are rejected. Bearer tokens belong in the client's secret storage and Authorization header, never in model-visible text or tool results.

## Errors and retries

JSON resource errors use `application/problem+json` with `type`, `title`, `status`, `code` and `instance`. OAuth endpoints use OAuth error responses. Missing version preconditions return 428, stale versions/reviews 412, missing authentication 401, authorization/CSRF failures 403, unknown public listings 404 and rate limiting 429. Read the draft again after a stale version; review again if publication terms changed. Do not silently retry a different action with an old approval.

Retry the same write with the same Idempotency-Key and body after transport uncertainty. Never reuse a key with changed data. The publication form's signed review supplies its stable operation identifier, so repeating the same confirmed publication returns the same canonical result. Receipts distinguish publication confirmation from the existing private event evidence and independently witnessed deal finalization.

The design follows [OpenAPI 3.1.1](https://spec.openapis.org/oas/v3.1.1.html), [OAuth authorization code with PKCE](https://www.rfc-editor.org/rfc/rfc7636.html), [dynamic client registration](https://www.rfc-editor.org/rfc/rfc7591.html), [OAuth security guidance](https://www.rfc-editor.org/rfc/rfc9700.html), and [Problem Details](https://www.rfc-editor.org/rfc/rfc9457.html). It does not claim certification or compatibility with every assistant platform.

## Accessibility diagnostics

The homepage and documentation are public HTML or text. Query parameters do not change homepage routing. There is no application-level bot challenge or user-agent allowlist on public reads. Authentication, CSRF checks, ETags and rate limits still protect writes; `/ready` is intentionally private. Cloudflare fronts the site, so test actual HTTP responses before attributing a tool error to the origin or to edge policies.

A useful reproduction records UTC time, requested URL, DNS answers, TLS certificate validation, HTTP status, Location and Content-Type headers, plus any Cloudflare Ray ID. Use GET without following redirects first. Test from an independent network rather than a LAN resolver, which can use split DNS. Compare an unrelated control domain when diagnosing an execution environment's DNS failures.

A web-reading tool reporting DisabledError or "not accessible via this tool" supplies no confirmed destination HTTP status. That can coexist with successful external HTTPS probes. Neither result proves that every assistant or network can access the site. No automatic transfer of ChatGPT attachments, unrestricted outbound chat requests or universal assistant compatibility is claimed.
