{"openapi":"3.1.0","info":{"title":"Handoff: File Sharing for AI Agents","version":"1.0.0","description":"Handoff: File Sharing for AI Agents. Save a file, then create a share link with expiry, open limits, and approval. The share url is returned only when the grant is created. Save it. GET /healthz and GET /api/healthz are liveness. GET /readyz checks Postgres, schema, and object storage."},"servers":[{"url":"https://hand-off.space"}],"security":[{"bearerAuth":[]}],"paths":{"/healthz":{"get":{"operationId":"healthCheck","security":[],"summary":"Liveness. Does not check dependencies. Also served at /api/healthz.","responses":{"200":{"description":"Process is up","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthStatus"}}}}}}},"/readyz":{"get":{"operationId":"readiness","security":[],"summary":"Checks Postgres, schema tables, and object storage. Also served at /api/readyz.","responses":{"200":{"description":"Dependencies are reachable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReadyStatus"}}}},"503":{"description":"A named dependency failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReadyStatus"}}}}}}},"/v1/signup":{"post":{"operationId":"signup","security":[],"summary":"Create an unclaimed account and return its key once","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupInput"}}}},"responses":{"201":{"description":"Account created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupResult"}}}},"429":{"$ref":"#/components/responses/Error"}}}},"/v1/files":{"post":{"operationId":"saveFile","summary":"Save base64 bytes privately; 10MB unclaimed or 50MB claimed per file","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FileInput"}}}},"responses":{"201":{"description":"Saved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/File"}}}},"403":{"$ref":"#/components/responses/Error"},"413":{"$ref":"#/components/responses/Error"}}},"get":{"operationId":"listFiles","summary":"List newest 1000 active files","responses":{"200":{"description":"Files","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/File"}}}}}}}},"/v1/files/{id}":{"parameters":[{"$ref":"#/components/parameters/Id"}],"get":{"operationId":"getFile","responses":{"200":{"description":"Metadata","content":{"application/json":{"schema":{"$ref":"#/components/schemas/File"}}}},"404":{"$ref":"#/components/responses/Error"}}},"delete":{"operationId":"deleteFile","responses":{"204":{"description":"Bytes deleted and metadata soft-deleted; grants no longer work"},"404":{"$ref":"#/components/responses/Error"}}}},"/v1/files/{id}/grants":{"parameters":[{"$ref":"#/components/parameters/Id"}],"post":{"operationId":"createGrant","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GrantInput"}}}},"responses":{"201":{"description":"Share created. Save url and token; later reads do not return them.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareResult"}}}}}}},"/v1/grants/{id}":{"parameters":[{"$ref":"#/components/parameters/Id"}],"get":{"operationId":"getGrant","responses":{"200":{"description":"Grant status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Grant"}}}}}}},"/v1/grants/{id}/revoke":{"parameters":[{"$ref":"#/components/parameters/Id"}],"post":{"operationId":"revokeGrant","responses":{"200":{"description":"Revoked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Grant"}}}}}}},"/v1/grants/{id}/approve":{"parameters":[{"$ref":"#/components/parameters/Id"}],"post":{"operationId":"approveGrant","responses":{"200":{"description":"Approved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Grant"}}}}}}},"/v1/grants/{id}/deny":{"parameters":[{"$ref":"#/components/parameters/Id"}],"post":{"operationId":"denyGrant","responses":{"200":{"description":"Denied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Grant"}}}}}}},"/v1/grants/{id}/logs":{"parameters":[{"$ref":"#/components/parameters/Id"}],"get":{"operationId":"getAccessLogs","summary":"Latest 1000 access attempts, newest first","responses":{"200":{"description":"Audit log","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccessLog"}}}}}}}},"/g/{token}":{"parameters":[{"$ref":"#/components/parameters/Token"}],"get":{"operationId":"downloadFile","security":[],"description":"Agent audience requires the API key of the audience_value agent_id, as Authorization Bearer, a bare Authorization value, or X-Handoff-Api-Key. HEAD is refused without consuming an open.","parameters":[],"responses":{"200":{"description":"File attachment","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"410":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"}}}},"/mcp":{"post":{"operationId":"mcpRequest","description":"Stateless MCP Streamable HTTP; initialize, tools/list and tools/call. Accept application/json and text/event-stream.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcMessage"}}}},"responses":{"200":{"description":"JSON-RPC response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RpcMessage"}}}},"202":{"description":"Notification accepted"}}}},"/llms.txt":{"get":{"operationId":"getLlms","security":[],"responses":{"200":{"description":"Plain text agent instructions"}}}},"/openapi.json":{"get":{"operationId":"getOpenApi","security":[],"responses":{"200":{"description":"OpenAPI 3.1 specification"}}}},"/.well-known/agent.json":{"get":{"operationId":"getAgentCard","security":[],"responses":{"200":{"description":"Agent discovery card; MCP service, not an A2A task endpoint"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"parameters":{"Id":{"name":"id","in":"path","required":true,"schema":{"type":"string"}},"Token":{"name":"token","in":"path","required":true,"schema":{"type":"string"}}},"responses":{"Error":{"description":"Request refused","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"HealthStatus":{"type":"object","required":["status"],"properties":{"status":{"type":"string"}}},"ReadyStatus":{"type":"object","required":["status","checks"],"properties":{"status":{"type":"string"},"checks":{"type":"object","required":["postgres","schema","storage"],"properties":{"postgres":{"type":"string"},"schema":{"type":"string"},"storage":{"type":"string"}}}}},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"signup":{"type":"string","description":"Present on HTTP 401. How to create an API key."}}},"RpcMessage":{"type":"object","additionalProperties":true},"SignupInput":{"type":"object","properties":{"agent_id":{"type":"string","minLength":1,"maxLength":80}}},"SignupResult":{"type":"object","required":["account_id","api_key","agent_id"],"properties":{"account_id":{"type":"string"},"api_key":{"type":"string"},"agent_id":{"type":"string"},"caps":{"type":"object","additionalProperties":{"type":"integer"}}}},"EmailInput":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"}}},"FileInput":{"type":"object","required":["name","content_base64"],"properties":{"name":{"type":"string","minLength":1,"maxLength":255},"mime":{"type":"string","default":"application/octet-stream"},"content_base64":{"type":"string","maxLength":70000000}}},"File":{"type":"object","required":["id","account_id","agent_id","name","mime","size","sha256","created_at"],"properties":{"id":{"type":"string"},"account_id":{"type":"string"},"agent_id":{"type":"string"},"name":{"type":"string"},"mime":{"type":"string"},"size":{"type":"integer"},"sha256":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"deleted_at":{"type":["string","null"],"format":"date-time"}}},"GrantInput":{"type":"object","required":["audience"],"properties":{"audience":{"type":"string","enum":["anyone_with_link","agent"]},"audience_value":{"type":"string","description":"Required when audience is agent. The recipient's globally unique registered agent_id.","and for email when that audience is enabled. Agent uses a globally unique registered agent_id.":null},"expires_in_hours":{"type":"number","exclusiveMinimum":0,"maximum":87600},"max_opens":{"type":"integer","minimum":1,"maximum":2147483647},"requires_approval":{"type":"boolean","default":false}}},"ShareResult":{"type":"object","required":["grant_id","url","token","token_prefix"],"properties":{"grant_id":{"type":"string"},"url":{"type":"string","description":"Full download URL. Returned only when the grant is created. Save it."},"token":{"type":"string","description":"Raw share token. Returned only when the grant is created. Save it with the url."},"token_prefix":{"type":"string","description":"First 8 hex characters of the token. Not a secret and not enough to download."}}},"Grant":{"type":"object","required":["id","file_id","token_prefix","audience","open_count","requires_approval","approval_status","created_at"],"properties":{"id":{"type":"string"},"file_id":{"type":"string"},"token_prefix":{"type":"string","description":"First 8 hex characters of the share token. Not a secret."},"audience":{"type":"string"},"audience_value":{"type":["string","null"]},"expires_at":{"type":["string","null"],"format":"date-time"},"max_opens":{"type":["integer","null"]},"open_count":{"type":"integer"},"requires_approval":{"type":"boolean"},"approval_status":{"type":"string","enum":["pending","approved","denied"]},"revoked_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"}}},"AccessLog":{"type":"object","required":["id","opened_at","result"],"properties":{"id":{"type":"string"},"grant_id":{"type":["string","null"]},"opened_at":{"type":"string","format":"date-time"},"ip":{"type":["string","null"]},"user_agent":{"type":["string","null"]},"result":{"type":"string","enum":["ok","expired","revoked","limit","pending_approval","denied","unauthorized","verification_required","storage_unavailable","not_found"]}}}}}}