cputools logo cputools

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 and arrive as a key + a link:

{ "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

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

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).

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 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:

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 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 tier — durable, shareable, witnessed, priced.