Skip to Content
Responses API

Responses API

POST /v1/responses

Matches OpenAI’s Responses endpoint for multimodal input/output with all providers.

PickLayer-specific parameters

ParameterTypeDefaultDescription
service_tierstring—Priority/flex tier. 'priority' (fast), 'auto' (standard default), 'flex' (lower-cost). See Service Tier.

Basic Request

{ "model": "openai/gpt-4.1-mini", "input": [ { "type": "input_text", "text": "Summarize this image" }, { "type": "input_image", "image_url": { "url": "<file-uuid>", "detail": "high" } } ], "response_format": { "type": "json" }, "max_output_tokens": 256 }

Content parts

The input array carries content parts. The supported part types are:

TypeDescription
input_textPlain text. { "type": "input_text", "text": "..." }
input_imageImage attachment by PickLayer UUID. { "type": "input_image", "image_url": { "url": "<picklayer-uuid>", "detail": "auto"|"low"|"high" } }
input_audioAudio attachment by PickLayer UUID. { "type": "input_audio", "input_audio": { "url": "<picklayer-uuid>", "format": "wav" } }
input_fileDocument attachment (PDF, DOCX, XLSX, MD, TXT, CSV, JSON, code, etc.). See below.

Unknown content types return 400 invalid_value with a precise param path. Parts are never silently dropped.

input_file (document attachments)

Canonical-rejoin change. input_file is part of the canonical OpenAI Responses API surface; PickLayer was previously off-canonical by silently dropping it during input normalization. This change rejoins canon — clients sending standard OpenAI Responses requests with input_file parts now get the documented behavior (200 with the file routed to the model, or a precise 400 with the offending param).

The Responses API accepts an input_file content part for document attachments. Three forms:

// Form A — reference a previously-uploaded PickLayer file (recommended). // Must be a PickLayer UUID. Provider-native file IDs are rejected with 400. { "type": "input_file", "file_id": "2d5a5194-105c-4d4b-ab46-d2450f447d4f" } // Form B — inline base64 (small docs only). { "type": "input_file", "filename": "report.pdf", "file_data": "data:application/pdf;base64,JVBERi0xLjQKJ..." } // Form C — external URL passthrough. { "type": "input_file", "file_url": "https://example.com/policy.pdf" }

Per-provider routing. PickLayer translates from the canonical input_file shape to each provider’s native form:

File classOpenAIAnthropicOther
PDFNative input_file with file_url (signed CDN URL) preferred; falls back to inline file_data. PDFs receive both text and per-page images.document block (source: { type: "url"|"base64"|"file" }).Adapter-specific.
Plain text (.txt, .md, .csv, .tsv, .json, .yaml, .xml, .html, code)Server-side UTF-8 read, inlined as input_text with <file:filename> framing.Inlined as text block.Inlined as text.
Office (.docx, .xlsx, .pptx)Server-side text extraction (mammoth/xlsx/officeparser); inlined as text. (Or native input_file with purpose: user_data where supported.)Server-side extraction; inlined as text.Server-side extraction.
Unsupported MIME for the chosen model400 invalid_value with param pointing at the offending content part. Never silently dropped.Same.Same.

The model’s accepted_extensions and accepted_mime_prefixes (from /v1/models) reflect what the runtime pipeline can actually route — no aspirational entries.

When the referenced file has is_encrypted: true, supply the per-file key in the request’s top-level file_keys map. The same encryption pipeline used for images and audio applies — both inline and signed-CDN-URL paths are supported.

Example — PDF attachment via PickLayer UUID:

curl -sS -X POST "$BASE/v1/responses" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-4o", "input": [{ "role": "user", "content": [ { "type": "input_text", "text": "Please summarize the attached file." }, { "type": "input_file", "file_id": "2d5a5194-105c-4d4b-ab46-d2450f447d4f" } ] }] }'

Attaching encrypted files

Canonical-additive extension. file_keys is a PickLayer-only top-level field. Clients sending canonical OpenAI Responses requests with plaintext files are unaffected — the field has no effect unless a referenced file has is_encrypted: true. End-to-end file encryption is not part of any canonical chat API; this field exists to preserve the trust model (UMK never leaves the client) without forking the canonical request shape.

When a chat request references a PickLayer file with is_encrypted: true, the gateway needs the per-file key to decrypt before forwarding plaintext to the upstream provider. The UMK never leaves the client, so the client supplies a per-file key per-request via the new top-level file_keys field. See Encryption — Attaching encrypted files to chat requests for the full architectural reference.

Field shape

{ "model": "openai/gpt-4o", "input": [ { "type": "input_text", "text": "What animal is in this image?" }, { "type": "input_image", "image_url": { "url": "<picklayer-uuid>" } } ], "file_keys": { "<picklayer-uuid>": "<base64url-derived-key>" } }
  • file_keys is a top-level object: { [picklayerFileUuid]: base64urlPerFileKey }.
  • One entry per file (not per reference). A UUID referenced multiple times in the same request appears once in file_keys.
  • Plaintext files do not appear here. A stray entry returns 400 unexpected_file_key.
  • The map is request-scoped and never persisted (logger redaction enforced).

Path selection

MethodWhenWire shape to provider
Inline data URLBytes ≤ provider maxInlineBytes.data:image/jpeg;base64,… (server decrypts in-process).
Signed CDN URLBytes exceed inline limit, or adapter prefers URL.CloudFront URL with opaque &efk=… (AES-256-GCM-wrapped per-file key). Origin decrypts on fetch. The plaintext file_key is never present in the URL.

The selection is automatic based on size and provider capability — the caller does not choose.

cURL example

curl -sS -X POST "$BASE/v1/responses" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-4o", "input": [ { "type": "input_text", "text": "What animal is in this image?" }, { "type": "input_image", "image_url": { "url": "6fefb90d-71fe-49ab-8f8f-2c5b4f2e0a01", "detail": "high" } } ], "file_keys": { "6fefb90d-71fe-49ab-8f8f-2c5b4f2e0a01": "Ei5dZbVaKQlnhtdtKuecl78o95fryNJPUvcQ9mSmk3c" }, "stream": true }'

Python SDK

from picklayer import PickLayerClient from picklayer.crypto import derive_umk_from_password, derive_file_key client = PickLayerClient(org_handler="acme", app_handler="myapp", token=token) # Unlock UMK once per session. keyring = client.keyring_status(include_keyring=True).data["keyring_data"] umk = derive_umk_from_password("user-password", keyring) # Re-derive each per-file key from the UUID + UMK on demand. file_id = "6fefb90d-71fe-49ab-8f8f-2c5b4f2e0a01" file_key = derive_file_key(umk, file_id) resp = client.responses( model="openai/gpt-4o", input=[ {"type": "input_text", "text": "What animal is in this image?"}, {"type": "input_image", "image_url": {"url": file_id, "detail": "high"}}, ], file_keys={file_id: file_key}, stream=True, )

The Python SDK can also auto-populate file_keys if you pass umk= directly to client.responses() — it walks the input tree, identifies encrypted UUIDs, re-derives each key from the UMK, and attaches the map. Pass the explicit map when you want to use a different UMK or have keys already cached.

Failure modes

ConditionHTTPcodeparam
Encrypted UUID, no file_keys entry400missing_file_keyfile_keys.<uuid>
Wrong key (fingerprint mismatch)400e2ee_invalid_file_keyfile_keys.<uuid>
Plaintext UUID with stray entry400unexpected_file_keyfile_keys.<uuid>
Legacy V1 encryption400e2ee_legacy_encryptionfile_keys.<uuid>
Tampered efk on signed URL400 (origin)e2ee_invalid_file_key—

The gateway fails closed in every case — the provider is not called, no billing event is generated.

Cross-session re-derivation

To resume an old thread that referenced encrypted files: fetch the message history (each message carries the original file_ids), recover the UMK from the local keyring, and call derive_file_key(umk, file_id) for each referenced UUID. There is no server-side key registry to sync — keys are recomputable from (UMK, file_id) alone.

Orthogonal to encrypt_content

The file_keys field is independent from encrypt_content:

  • file_keys controls inbound decryption — encrypted file references on the request.
  • encrypt_content: true controls outbound encryption of stored response content/output (sealed envelope V2 — see End-to-End Encrypted Responses).

You can use either, neither, or both.

Usage Shape

The non-streaming response and the final response.done streaming event carry an OpenAI-native usage object. Cached prompt tokens and reasoning tokens are nested:

{ "usage": { "input_tokens": 1245, "output_tokens": 412, "total_tokens": 1657, "input_tokens_details": { "cached_tokens": 800 }, "output_tokens_details": { "reasoning_tokens": 96 } } }
  • input_tokens_details.cached_tokens — present when the request hit the prompt cache. Both the live and retrieve flows use this nested shape; the canonical flat field cached_input_tokens is not forwarded to the customer surface.
  • output_tokens_details.reasoning_tokens — present for thinking models (e.g. o1, GPT-5 reasoning, Claude extended thinking).
  • Both sub-objects are zero-suppressed when their fields would be 0.

See Prompt Caching for the full cross-endpoint cache-field reference.

Streaming Events

The PickLayer /v1/responses SSE event surface matches the OpenAI Responses API streaming spec :

  • response.output_text.delta - Text chunk (delta field)
  • response.reasoning.delta - Reasoning content (thinking models)
  • response.output_item.added - Output item envelope opened (for function_call: carries call_id, name, status: "in_progress")
  • response.function_call_arguments.delta - Tool call argument chunk (delta field, partial JSON string)
  • response.function_call_arguments.done - Tool call arguments complete (arguments field, full JSON string)
  • response.output_item.done - Output item envelope closed (for function_call: carries status: "completed")
  • response.web_search_call.in_progress / .searching / .completed - Web search lifecycle
  • response.completed - Final response with usage (shape per Usage Shape)

Reasoning Controls

For models that support reasoning (like o1-preview):

{ "model": "openai/o1-preview", "input": "Solve this math problem...", "reasoning": { "effort": "high", "summary": "auto" } }

Response Storage & Retrieval

By default, responses are ephemeral — they stream back and are not persisted. Set store: true to save the response for later retrieval.

Storing a Response

{ "model": "openai/gpt-4o", "input": "Summarize the quarterly results", "store": true }

Storage is also triggered implicitly by:

  • background: true — creates a background job (returns 202 immediately)
  • conversation: { id: "..." } — links to a conversation thread
  • previous_response_id: "resp_..." — replies to a previous response

Stored responses expire after 30 days.

Retrieving a Stored Response

GET /v1/responses/:id

Accepts either a PickLayer UUID or a provider response ID (resp_*). Returns an OpenAI-compatible response object.

{ "id": "resp_abc123", "object": "response", "status": "completed", "created_at": 1710000000, "model": "openai/gpt-4o", "output": [ { "type": "message", "role": "assistant", "content": [ { "type": "output_text", "text": "The quarterly results show..." } ] } ], "usage": { "input_tokens": 42, "output_tokens": 256, "total_tokens": 298, "input_tokens_details": { "cached_tokens": 0 }, "output_tokens_details": { "reasoning_tokens": 0 } } }

(input_tokens_details / output_tokens_details are zero-suppressed and omitted entirely when both sub-fields are 0 — shown here for shape reference.)

Returns 404 if the response was not stored, has expired, or doesn’t belong to the current user.

Background Jobs

For long-running requests, use background: true to return immediately:

{ "model": "openai/gpt-4o", "input": "Write a detailed analysis...", "background": true, "max_output_tokens": 16384 }

Response (202 Accepted):

{ "id": "resp_abc123", "object": "response", "status": "queued", "metadata": { "job_id": "job_xyz" } }

Poll for completion:

GET /v1/responses/jobs/:jobId

End-to-End Encrypted Responses

Responses can be stored with E2EE. The server encrypts using the user’s public key and cannot decrypt. Only the client holding the matching private key can read the stored content.

Setup

1. Initialize keyring (one-time per user):

POST /keyring/init

2. Generate an X25519 keypair (client-side — private key never leaves the client).

3. Upload public key:

POST /keyring/public-key
{ "public_key": "<base64url-encoded X25519 public key>" }

Creating an Encrypted Response

Set encrypt_content: true. No umk field is needed — the server retrieves the public key from the keyring automatically.

encrypt_content: true encrypts stored response content, not arbitrary request metadata. Any metadata you send, plus other operational fields the system needs for routing/retrieval/usage tracking, remains plaintext and server-visible. Do not put secrets, raw prompt text, document excerpts, API keys, or sensitive user data in metadata.

{ "model": "openai/gpt-4o", "input": "Summarize the confidential report", "store": true, "encrypt_content": true }

Retrieving and Decrypting

GET /v1/responses/:id

Encrypted responses include an encryption object alongside the standard response fields. The content and output fields are ciphertext; operational metadata such as model, usage, timestamps, and any request metadata you supplied remains plaintext.

{ "id": "resp_abc123", "object": "response", "status": "completed", "encrypted": true, "encryption": { "version": 2, "envelope": { "ephemeral_public_key": "<base64url>", "wrapped_dek": "<base64url>", "wrapped_dek_nonce": "<base64url>", "alg": "x25519-xchacha20poly1305" }, "encrypted_fields": ["content", "output"], "encrypted_at": "2026-03-25T12:00:00Z" }, "usage": { "input_tokens": 42, "output_tokens": 256, "total_tokens": 298 } }

Decrypt on the client using the private key and the envelope fields.

Background Encrypted Jobs

{ "model": "openai/gpt-4o", "input": "Write analysis...", "background": true, "encrypt_content": true }

The background job encrypts before persisting. Poll GET /v1/responses/jobs/:jobId as normal; the completed response will include the encryption envelope.

Encryption Metadata Fields

FieldDescription
version2 for public-key encryption
envelope.ephemeral_public_keyServer’s ephemeral X25519 public key (base64url)
envelope.wrapped_dekInner DEK encrypted with the X25519 shared secret (base64url)
envelope.wrapped_dek_nonceNonce for wrapped_dek decryption
envelope.algx25519-xchacha20poly1305
sealed_dekContent DEK encrypted with the inner DEK (base64url)
sealed_dek_nonceNonce for sealed_dek decryption
encrypted_fieldsList of encrypted field names (e.g., ["content", "output"])
encrypted_atISO timestamp

Decryption Steps

The V2 envelope uses a two-layer DEK scheme. To decrypt:

  1. Derive shared secret: shared_secret = X25519(private_key, envelope.ephemeral_public_key)
  2. Unwrap inner DEK: inner_dek = XChaCha20-Poly1305.decrypt(envelope.wrapped_dek, shared_secret, envelope.wrapped_dek_nonce)
  3. Unwrap content DEK: content_dek = XChaCha20-Poly1305.decrypt(sealed_dek, inner_dek, sealed_dek_nonce)
  4. Decrypt each field: plaintext = XChaCha20-Poly1305.decrypt(field.ciphertext, content_dek, field.nonce)

Important: The wrapped_dek does not contain the content DEK directly. It contains an intermediate inner DEK which must first be used to unwrap sealed_dek to obtain the actual content DEK. Attempting to decrypt content fields with the inner DEK will produce a MAC authentication error.

Security Model

  • The server never possesses the private key
  • Each response uses a unique ephemeral keypair (forward secrecy per response)
  • Two-layer key wrapping isolates the content key from key-agreement material
  • Content is encrypted with XChaCha20-Poly1305 (AEAD)
  • Only content and output are encrypted; model, usage, timestamps, request metadata, and other operational metadata remain plaintext for billing and lifecycle tracking

Python SDK Example

from picklayer import PickLayerClient from picklayer.crypto import CryptoHelper # One-time setup client = PickLayerClient(org_handler="myorg", app_handler="myapp") client.set_token(token) # Unlock keyring and generate keypair crypto = CryptoHelper() crypto.derive_umk("password", keyring_data) crypto.generate_keypair() # Upload keypair to server (public key in plaintext, private key wrapped with UMK) wrapped_sk = crypto.wrap_and_store_private_key() client.keyring_upload_keypair(crypto.public_key, wrapped_sk) # Create encrypted response resp = client.responses( {"model": "openai/gpt-4o", "input": "Confidential analysis"}, store=True, encrypt_content=True, ) # Retrieve and decrypt (on any device — just need your password) keyring = client.keyring_get_data() crypto.derive_umk("password", keyring.data["keyring_data"]) crypto.load_private_key(keyring.data["wrapped_private_key"]) stored = client.get_response(resp.data["id"]) if stored.data.get("encrypted"): decrypted = crypto.decrypt_response_auto( stored.data, stored.data["encryption"] )

Cancel & Delete

EndpointDescription
POST /v1/responses/:id/cancelCancel an in-progress response. Returns 409 if already completed.
DELETE /v1/responses/:idHard-delete a stored response (returns 204).

Response Statuses

StatusDescription
queuedJob created, waiting to start
in_progressCurrently generating
completedSuccessfully finished
failedGeneration failed
cancelledCancelled by user
incompleteStopped early (e.g., max tokens reached)

Python SDK

# Store a response resp = client.responses( model="openai/gpt-4o", input="Summarize the results", store=True ) response_id = resp.data["id"] # Retrieve later stored = client.get_response(response_id) # Background job resp = client.responses( model="openai/gpt-4o", input="Write a detailed report...", background=True ) result = client.wait_for_job(resp.data["metadata"]["job_id"]) # Cancel client.cancel_response(response_id) # Delete client.delete_response(response_id)

client.responses(..., encrypt_content=True) follows the same rule as the raw API: stored response content is encrypted, but request metadata and other operational metadata remain plaintext/server-visible. Treat metadata as non-secret.

Last updated on