# Document sharing

Documents are private by default. A document you create through `/v1` or through an MCP tool is readable by your company and nobody else until someone turns sharing on.

Turning sharing on is publishing. It mints a secret, and the resulting link is readable by **anyone on the internet who has the URL**: no sign-in, no company membership, no expiry.

## Creating estimates and bids

`POST /v1/projects/{id}/estimates` and `POST /v1/projects/{id}/bids` save the document private and return the signed-in path in `documentUrl`.

A REST caller opts into a public link with `share: true` on the request body:

```bash
curl -X POST https://api.exayard.com/v1/projects/{id}/bids \
  -H "Authorization: Bearer exa_live_..." \
  -H "Content-Type: application/json" \
  -d '{"share":true}'
```

The TypeScript SDK takes the same flag on `estimates.create` and `bids.create`:

```typescript
import { Exayard } from '@exayard/sdk'

// The estimator blocks for 30–60s, so raise the default 60s timeout.
const exa = new Exayard({ apiKey: process.env.EXAYARD_API_KEY!, timeoutMs: 180_000 })

const bid = await exa.bids.create('<projectId>', { share: true })
```

With `share: true`, `documentUrl` is the public path instead of the signed-in one. You can also share or unshare later with the endpoint below, so `share: true` is a convenience, never the only way.

The streaming sibling `POST /v1/projects/{id}/estimates/generate` persists no document, so it has no `share` flag.

The **MCP tool schemas for those two operations have no `share` flag at all**. The caller on that surface is a language model, and a model is not allowed to mint a public link on its own initiative. An agent that needs one asks the user and then calls `set_document_sharing`.

## Turning sharing on and off

`PATCH /v1/documents/{id}/share` flips the switch. Scope: `write:projects`.

```bash
curl -X PATCH https://api.exayard.com/v1/documents/{id}/share \
  -H "Authorization: Bearer exa_live_..." \
  -H "Content-Type: application/json" \
  -d '{"enabled":true}'
```

Enabling returns the link material:

```json
{
  "id": "kx71w9n2p4bd6vqzr0m3jscty5h8f2ea",
  "name": "Bid for Elm St",
  "shareEnabled": true,
  "shareSecret": "9f3c1ab2d7e04c5f",
  "sharePath": "/documents/kx71w9n2p4bd6vqzr0m3jscty5h8f2ea-9f3c1ab2d7e04c5f"
}
```

In the SDK that's `documents.setSharing`, which returns the same object typed as `DocumentShare`:

```typescript
const share = await exa.documents.setSharing('<documentId>', { enabled: true })

// shareSecret and sharePath are optional on the type, because they are absent
// whenever sharing is off.
if (share.shareEnabled && share.sharePath) {
  console.log(`https://exayard.com${share.sharePath}`)
}
```

Use `documents.list({})` or `documents.listByProject(projectId)` to find a document id you didn't just generate.

The examples use a key set to work in **This company**, so they name no company. A key that works in every company
that installed its app adds `organizationId` (or the `Exayard-Organization-Id` header). See
[Company scope](https://developers.exayard.com/apps-and-keys.md#company-scope).

Disabling returns `shareEnabled: false` and **no `shareSecret` and no `sharePath`**. The secret is what makes the link work, so we never echo it back for a document that isn't shared. Don't write a client that reads `sharePath` unconditionally.

A document id that belongs to another company answers 404, exactly like an id that doesn't exist, so a 404 never tells you whether the id is real somewhere else.

Current state is readable on `GET /v1/documents/{id}`: `shareEnabled` and `shareSecret` come back on the document.

## The secret survives a disable

Disabling revokes access but keeps the secret stored on the document. Re-enabling later reuses that same secret, so **the old URL starts working again**. This is deliberate: a customer who toggles sharing off and back on doesn't invalidate the link they already emailed their client.

The consequence for integrators: disabling is not the same as rotating. There is no rotate operation on this endpoint. If a link has leaked, treat the document itself as burned rather than assuming a disable/enable cycle produced a fresh URL.

## Paths are relative

`sharePath` and `documentUrl` are app-relative paths, not absolute URLs, and always have been. Prepend the app host yourself:

```typescript
const url = `https://exayard.com${res.sharePath}`
```

## The MCP tool needs an explicit confirmation to enable

`set_document_sharing` wraps the same handler and adds one argument, `confirmed`.

- `enabled: true` **without** `confirmed: true` writes nothing. The tool returns `{ requiresConfirmation: true, documentId, message }`, and the message tells the model to ask the user before calling again with `confirmed: true`.
- `enabled: false` is ungated. Disabling narrows exposure, so it never needs a confirmation.

That asymmetry is a product rule, not an oversight. Enabling publishes; disabling un-publishes. Build agent flows expecting the first enable call to come back as a confirmation prompt rather than a result, and expect a second call after the user says yes.

REST callers don't see this gate. There, the `enabled` flag is itself the intent, because the caller is a human-written integration rather than a model.
