
# Receiving outputs

Every binary-producing op returns the same envelope. Small results arrive **inline** (the gate is the base64-encoded size against a 4 MiB threshold — raw outputs up to ~3 MiB, since base64 inflates ~1.37×); larger ones land in your free 24-hour [scratch tier](/docs/persistence-tiers) and arrive as a key + a link:

```json
{ "output": { "inline": "<base64>", "sizeBytes": 51234, "contentType": "application/pdf", "filename": "converted.pdf" } }
{ "output": { "outputKey": "scratch/<you>/<uuid>", "outputUrl": "https://…presigned, valid 1 hour…", "sizeBytes": 12023329, "contentType": "image/png" } }
```

Handle both shapes and you handle every op. Recipes per client:

## curl / shell — to disk

```bash
RESP=$(curl -s -X POST https://api.relaystation.ai/v1/pdf/merge \
  -H "Authorization: Bearer $KEY" -H "Idempotency-Key: $(uuidgen)" \
  -d @payload.json)

if echo "$RESP" | jq -e '.output.inline' > /dev/null; then
  echo "$RESP" | jq -r '.output.inline' | base64 -d > result.pdf
else
  curl -s -o result.pdf "$(echo "$RESP" | jq -r '.output.outputUrl')"
fi
```

The `outputUrl` is presigned — no auth header on that GET (and don't send one; it's a direct storage URL, not an API route).

## Code (fetch) — in an agent sandbox

```js
const { output } = await (await fetch(url, opts)).json();
const bytes = output.inline
  ? Buffer.from(output.inline, 'base64')
  : Buffer.from(await (await fetch(output.outputUrl)).arrayBuffer());
```

If your sandbox blocks outbound domains, allow the API host **and** the storage host the `outputUrl` points at (it is a different domain).

## Chat-based clients (MCP) — present the link

In chat clients the tool result is JSON in the conversation; nobody wants 12 MB of base64 there. For large outputs, surface `outputUrl` to the human as a download link — it works in a browser for one hour. If the session outlives the link, re-fetch a fresh one (below) or chain the `outputKey` into a durable [baton](https://relaystation.ai/docs/batons) when the result must persist.

MCP tool results for a stored binary output also carry a **`resource_link`** content block alongside the JSON — `{ "type": "resource_link", "uri": "<presigned URL>", "name": "<filename>", "mimeType": "<contentType>" }` (MCP spec 2025-06). A client that understands resource links can offer the download directly; one that doesn't simply ignores the extra block — the JSON (and `structuredContent`) still carry `outputKey` + `outputUrl`, so nothing is lost.

## Re-fetching a stored output

The `outputUrl` an op returns expires in an hour. To get a **fresh** link for the same object without re-running the op, call:

```bash
curl -s https://api.relaystation.ai/v1/outputs/scratch/<you>/<uuid> \
  -H "Authorization: Bearer $KEY"
# → { "outputKey", "url": "<fresh presigned GET, 1 hour>", "sizeBytes", "contentType", "expiresAt" }
```

This works for as long as the scratch object lives (24 hours), then returns `404`. It is **free**, and ownership-gated: you can only re-fetch keys under your own `scratch/<you>/` namespace — anyone else's key (or an aged-out one) returns `404`, never a hint that it exists. The `url` it returns is the immediate download; `outputUrl` from the original response stays valid for its hour too.

## Chaining instead of downloading

If the next consumer is another Relaystation op, don't download at all — pass `outputKey` as the next call's `inputKey`. Bytes stay on the platform; see [persistence tiers](/docs/persistence-tiers) for the worked example.

## The honest clock

- **`outputUrl`** is valid for **1 hour** from the response.
- **`outputKey`** (the scratch object behind both) lives **24 hours**, then auto-deletes.
- Need it longer? That is the [baton](https://relaystation.ai/docs/batons) tier — durable, shareable, witnessed, priced.
