{
  "openapi": "3.0.3",
  "info": {
    "title": "Human For AI API",
    "version": "1.9.0",
    "description": "A human endpoint for AI agents. Submit tasks that need physical presence, human perception, or human judgment — real-world verification, product testing, AI output review, data collection, local physical-world errands — and a verified human operator performs them. All services are FREE during the proof-of-concept pilot. No authentication is required; every task is reviewed by the human before acceptance, and illegal, harmful, deceptive, unsafe, or privacy-invasive tasks are rejected. First response within 12 hours, any day of the week. An MCP server is also available at https://humanforai.dev/mcp (streamable HTTP, no auth; discovery at /.well-known/mcp). Client libraries: Python `pip install humanforai` (client + `humanforai` CLI, https://pypi.org/project/humanforai/), Go `go get github.com/humanforai/humanforai-go` (client + CLI, https://pkg.go.dev/github.com/humanforai/humanforai-go), Node `npx -y humanforai` (stdio MCP proxy).\n\nAsynchronous jobs: task submission is a long-running human job. POST /api/v1/tasks returns 202 Accepted immediately with a task_id (the job identifier), a status_url, and a Location header pointing at the poll endpoint; poll GET /api/v1/tasks/{task_id} until status reaches delivered or rejected. Work never completes within the submission request.\n\nIdempotency: POST /api/v1/tasks and POST /api/v1/messages accept an optional Idempotency-Key header (up to 200 chars). Retrying with the same key and identical payload within 24h replays the original response (marked Idempotency-Replayed: true) instead of creating a duplicate or burning rate-limit budget; the same key with a different payload returns 422. Keys are scoped per client.\n\nPagination: list endpoints use cursor pagination — pass limit (1–50) and the opaque next_cursor from the previous page; next_cursor is null on the last page. See GET /api/v1/services.\n\nVersioning & deprecation: the API is versioned in the URL path (/api/v1). Changes within a version are additive only; breaking changes ship as a new path version, and a deprecated version keeps working for at least 90 days while announcing retirement with Deprecation and Sunset headers (RFC 8594) plus a notice in /agent.json. Nothing is currently deprecated. Policy: https://humanforai.dev/developers#versioning.\n\nRate limits: responses carry standard RateLimit-Policy / RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset headers (draft-ietf-httpapi-ratelimit-headers), and every 429 includes Retry-After. Details: https://humanforai.dev/developers#rate-limits.",
    "contact": {
      "name": "Human For AI operator",
      "url": "https://humanforai.dev/contact"
    }
  },
  "externalDocs": {
    "description": "Human-readable API documentation",
    "url": "https://humanforai.dev/api"
  },
  "servers": [
    {
      "url": "https://humanforai.dev",
      "description": "Production"
    }
  ],
  "paths": {
    "/api/v1": {
      "get": {
        "operationId": "apiIndex",
        "summary": "API root: JSON index of the public operations",
        "description": "Names every public operation with its method, path and a one-line summary, plus links to this document, the Markdown docs, the MCP endpoint and the agent manifest. Carries the same RateLimit headers as every other API response.",
        "responses": {
          "200": {
            "description": "The operation index.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "name": { "type": "string", "example": "Human For AI REST API" },
                    "version": { "type": "string", "example": "v1" },
                    "authentication": { "type": "string", "example": "none" },
                    "pricing": { "type": "string", "example": "free_pilot" },
                    "openapi": { "type": "string", "format": "uri" },
                    "documentation": { "type": "string", "format": "uri" },
                    "operations": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "method": { "type": "string", "example": "POST" },
                          "path": { "type": "string", "example": "/api/v1/tasks" },
                          "summary": { "type": "string" }
                        }
                      }
                    },
                    "mcp": { "type": "string", "format": "uri" },
                    "manifest": { "type": "string", "format": "uri" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tasks": {
      "post": {
        "operationId": "submitTask",
        "summary": "Submit a task for the human operator (async job)",
        "description": "Submits a task as an asynchronous job: the 202 Accepted acknowledges acceptance into the queue, never completion. The response body carries the job identifier (task_id) and status_url, and the Location header points at the same poll endpoint — poll GET /api/v1/tasks/{task_id} until status reaches delivered or rejected. Keep the task_id: it is the only way to check status (the ID acts as an unlisted token). The task is reviewed by the human operator before acceptance. Include contact_email to receive the deliverable and any questions. Send an Idempotency-Key header to make retries safe.",
        "parameters": [
          { "$ref": "#/components/parameters/IdempotencyKey" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TaskRequest"
              },
              "example": {
                "task_type": "real_world_verification",
                "description": "Verify whether a specific product exists in a local store and photograph the shelf price.",
                "location_required": true,
                "output_format": "text_report_with_photos",
                "contact_email": "agent-results@example.com",
                "requester": "acme-shopping-agent"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Async job accepted (202 Accepted): the task is queued for human review, not completed. Poll the URL in the Location header (also status_url in the body) until status is delivered or rejected.",
            "headers": {
              "Location": { "$ref": "#/components/headers/Location" },
              "Idempotency-Replayed": { "$ref": "#/components/headers/IdempotencyReplayed" },
              "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskSubmissionResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited: per-client hourly/daily cap or global daily capacity reached. Honor Retry-After before resubmitting.",
            "headers": {
              "Retry-After": { "$ref": "#/components/headers/RetryAfter" },
              "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Client address is on the abuse blocklist."
          },
          "409": {
            "description": "Duplicate: an identical submission exists within the last 24h; the response includes the existing id."
          }
        }
      }
    },
    "/api/v1/tasks/{task_id}": {
      "get": {
        "operationId": "getTaskStatus",
        "summary": "Check the status of a submitted task",
        "description": "Public status lookup. The task_id acts as the access token — anyone holding it can view status. Contact email is never returned.",
        "parameters": [
          {
            "name": "task_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "HFAI-2026-97BBAB04C15D2E63"
          }
        ],
        "responses": {
          "200": {
            "description": "Current task state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          },
          "404": {
            "description": "No task with that ID.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/messages": {
      "get": {
        "operationId": "sendMessageViaUrl",
        "summary": "Send a message via URL (fallback for GET-only callers)",
        "description": "The same contact channel as POST /api/v1/messages, driven entirely by query parameters — for agents whose tooling can fetch URLs but cannot issue a POST or set headers. Runs the identical pipeline: validation, MX check on reply_to, duplicate guard, and rate limits. An identical resend within 24h returns 409 carrying the original message_id, so retries are safe without an Idempotency-Key. Note: query strings traverse ordinary server logs — prefer POST when your tooling allows it.",
        "parameters": [
          { "name": "message", "in": "query", "required": true, "description": "The message text (same rules as the POST body field).", "schema": { "type": "string", "minLength": 5, "maxLength": 5000 } },
          { "name": "reply_to", "in": "query", "required": true, "description": "A real mailbox (MX-checked) — the operator's only way to answer.", "schema": { "type": "string", "format": "email" } },
          { "name": "from", "in": "query", "required": false, "description": "Who is writing — agent or product name.", "schema": { "type": "string" } },
          { "name": "subject", "in": "query", "required": false, "schema": { "type": "string" } },
          { "name": "requester", "in": "query", "required": false, "schema": { "type": "string" } }
        ],
        "responses": {
          "201": {
            "description": "Message received — identical semantics to the POST response.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MessageResponse" } } }
          },
          "409": {
            "description": "An identical message was received within the last 24h; the body carries the original message_id. Treat as success — the operator will reply to the original.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "422": {
            "description": "Validation failed (missing/short message, or reply_to missing, malformed, or without MX records).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "429": {
            "description": "Rate limited: per-client hourly/daily cap or global daily capacity reached. Honor Retry-After.",
            "headers": {
              "Retry-After": { "$ref": "#/components/headers/RetryAfter" }
            },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      },
      "post": {
        "operationId": "sendMessage",
        "summary": "Send a message to the human operator (opens a thread)",
        "description": "Structured contact channel for questions, scoping, or anything that is not yet a task. reply_to is required: an email (MX-checked) or an https webhook URL for signed pushes. Every message is also a pollable thread — the response carries thread_url and a one-time access_token, so the reply is readable via GET /api/v1/messages/{id} even without a mailbox. Send an Idempotency-Key header to make retries safe. GET-only callers: the same channel works as a URL — see the GET operation on this path.",
        "parameters": [
          { "$ref": "#/components/parameters/IdempotencyKey" }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MessageRequest"
              },
              "example": {
                "message": "Before I submit a task: can you test an Android app that requires a local SIM card?",
                "reply_to": "agent-inbox@example.com",
                "from": "acme-qa-agent",
                "subject": "Scoping question"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Message received.",
            "headers": {
              "Idempotency-Replayed": { "$ref": "#/components/headers/IdempotencyReplayed" },
              "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited: per-client hourly/daily cap or global daily capacity reached. Honor Retry-After before resubmitting.",
            "headers": {
              "Retry-After": { "$ref": "#/components/headers/RetryAfter" },
              "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Client address is on the abuse blocklist."
          },
          "409": {
            "description": "Duplicate: an identical submission exists within the last 24h; the response includes the existing id."
          }
        }
      }
    },
    "/api/v1/messages/{message_id}": {
      "get": {
        "operationId": "getMessageThread",
        "summary": "Read a message thread",
        "description": "The thread for a message you sent: the original text, every reply oldest-first, and whether the operator has answered. Authenticate with the access_token from the submission response — as 'Authorization: Bearer <token>' or '?token=<token>' for GET-only callers. A wrong id and a wrong token answer identically (404), so neither leaks the other. Reading is cheap, but the operator answers at human speed: poll occasionally, and read an empty replies list as no answer yet.",
        "parameters": [
          { "name": "message_id", "in": "path", "required": true, "schema": { "type": "string" }, "example": "MSG-2026-1A2B3C4D" },
          { "name": "token", "in": "query", "required": false, "description": "The thread access_token — alternative to the Authorization header for GET-only callers.", "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "The thread.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MessageThread" } } }
          },
          "404": {
            "description": "No readable thread for that id and token."
          }
        }
      },
      "post": {
        "operationId": "replyInMessageThread",
        "summary": "Follow up in a message thread",
        "description": "Add to a thread you opened — answer a question the operator asked, add detail, correct yourself, or withdraw the request. Prefer this over sending a fresh message about a subject already opened. Authenticate with the access_token ('Authorization: Bearer', '?token=', or a 'token' body field). The operator is notified of every follow-up.",
        "parameters": [
          { "name": "message_id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["message"],
                "properties": {
                  "message": { "type": "string", "minLength": 2, "maxLength": 5000 },
                  "token": { "type": "string", "description": "The thread access_token (alternative to the Authorization header)." }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Added to the thread.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message_id": { "type": "string" },
                    "reply_count": { "type": "integer" },
                    "message": { "type": "string" }
                  }
                }
              }
            }
          },
          "404": { "description": "No writable thread for that id and token." },
          "422": { "description": "Missing/too-long message, or the thread reached its reply cap." },
          "429": { "description": "Hourly message limit reached — honor Retry-After." }
        }
      }
    },
    "/api/v1/services": {
      "get": {
        "operationId": "listServices",
        "summary": "List the human service catalog (cursor-paginated)",
        "description": "Public, cursor-paginated list of the service types the human operator offers — the same entries as the `services` array in /agent.json. Pass `limit` (1–50, default 10) and the opaque `cursor` from the previous page's next_cursor; next_cursor is null on the last page. The catalog is examples, not limits — unlisted needs are welcome as custom_human_in_the_loop.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": { "type": "integer", "minimum": 1, "maximum": 50, "default": 10 },
            "description": "Page size."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": { "type": "string" },
            "description": "Opaque pagination cursor from the previous page's next_cursor. Omit for the first page."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of the service catalog.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ServiceCatalogPage" }
              }
            }
          },
          "422": {
            "description": "Invalid cursor.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ValidationError" }
              }
            }
          }
        }
      }
    },
    "/api/v1/services.md": {
      "get": {
        "operationId": "listServicesMarkdown",
        "summary": "Service catalog as Markdown",
        "description": "Markdown twin of GET /api/v1/services: the same entries as the services array in /agent.json, rendered as headings and bullet lists for agents that read *.md URLs. Not paginated. Cacheable for an hour.",
        "responses": {
          "200": {
            "description": "The catalog as text/markdown.",
            "content": {
              "text/markdown": {
                "schema": { "type": "string" }
              }
            }
          }
        }
      }
    },
    "/api/v1/health": {
      "get": {
        "operationId": "healthCheck",
        "summary": "Liveness check",
        "responses": {
          "200": {
            "description": "Service is up.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "ok"
                    },
                    "service": {
                      "type": "string",
                      "example": "human-for-ai"
                    },
                    "api_version": {
                      "type": "string",
                      "example": "1.8.0"
                    },
                    "time": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/agent.json": {
      "get": {
        "operationId": "getAgentManifest",
        "summary": "Machine-readable platform manifest",
        "description": "Full platform manifest: services catalog, endpoints, task schema, accepted and rejected task types, pricing, trust policy, and limits.",
        "responses": {
          "200": {
            "description": "The manifest.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentManifest"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "schema": { "type": "string", "maxLength": 200 },
        "description": "Optional client-supplied key (e.g. a UUID) that makes this write retry-safe. A retry with the same key and identical payload within 24h replays the original 201 response (Idempotency-Replayed: true) instead of creating a duplicate; the same key with a different payload returns 422. Keys are scoped per client.",
        "example": "3f8a1c2e-9d4b-4c6a-8e21-7b5f0d9a4e12"
      }
    },
    "headers": {
      "Location": {
        "description": "Poll endpoint for this asynchronous job (same value as status_url in the body): GET it until status is delivered or rejected.",
        "schema": { "type": "string", "example": "/api/v1/tasks/HFAI-2026-97BBAB04C15D2E63" }
      },
      "IdempotencyReplayed": {
        "description": "Present and 'true' when this response was replayed from a previous request with the same Idempotency-Key, rather than processed again.",
        "schema": { "type": "string", "example": "true" }
      },
      "RateLimitLimit": {
        "description": "Requests allowed for this client in the current daily window (draft-ietf-httpapi-ratelimit-headers).",
        "schema": { "type": "string", "example": "5" }
      },
      "RateLimitRemaining": {
        "description": "Requests remaining for this client in the current window.",
        "schema": { "type": "string", "example": "4" }
      },
      "RateLimitReset": {
        "description": "Seconds until the window resets (daily windows reset at 00:00 UTC).",
        "schema": { "type": "string", "example": "31742" }
      },
      "RetryAfter": {
        "description": "Seconds to wait before retrying (RFC 9110).",
        "schema": { "type": "string", "example": "3600" }
      }
    },
    "schemas": {
      "Service": {
        "type": "object",
        "properties": {
          "id": { "$ref": "#/components/schemas/TaskType" },
          "name": { "type": "string", "example": "Real-world verification" },
          "description": { "type": "string" }
        }
      },
      "ServiceCatalogPage": {
        "type": "object",
        "description": "One page of a cursor-paginated list. Follow next_cursor until it is null.",
        "properties": {
          "items": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Service" }
          },
          "total": { "type": "integer", "example": 10 },
          "next_cursor": {
            "type": "string",
            "nullable": true,
            "description": "Opaque cursor for the next page; null when this is the last page."
          }
        }
      },
      "AgentManifest": {
        "type": "object",
        "description": "The platform's canonical self-description. Additional descriptive fields appear as the platform evolves — treat unknown properties as informational.",
        "additionalProperties": true,
        "properties": {
          "brand_name": { "type": "string", "example": "Human For AI" },
          "domain": { "type": "string", "format": "uri", "example": "https://humanforai.dev" },
          "tagline": { "type": "string" },
          "description": { "type": "string" },
          "api_version": { "type": "string", "example": "1.8.0" },
          "last_updated": { "type": "string", "format": "date" },
          "interfaces": { "type": "array", "items": { "type": "string" } },
          "authentication": { "type": "string" },
          "task_request_endpoint": { "type": "string", "example": "/api/v1/tasks" },
          "contact_endpoint": { "type": "string", "example": "/api/v1/messages" },
          "response_expectations": { "type": "object", "additionalProperties": true },
          "pricing_model": { "type": "object", "additionalProperties": true },
          "services": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": { "$ref": "#/components/schemas/TaskType" },
                "name": { "type": "string" },
                "description": { "type": "string" }
              }
            }
          },
          "endpoints": { "type": "object", "additionalProperties": true },
          "accepted_task_types": { "type": "array", "items": { "$ref": "#/components/schemas/TaskType" } },
          "rejected_task_types": { "type": "array", "items": { "type": "string" } },
          "trust_and_safety": { "type": "object", "additionalProperties": true },
          "limits": { "type": "object", "additionalProperties": true },
          "signed_receipts": { "type": "object", "additionalProperties": true }
        }
      },
      "TaskType": {
        "type": "string",
        "enum": [
          "real_world_verification",
          "product_or_app_testing",
          "human_judgment_and_feedback",
          "data_collection",
          "local_physical_task",
          "ai_output_review",
          "prompt_and_workflow_testing",
          "simulation_and_automation_testing",
          "accessibility_and_usability_check",
          "custom_human_in_the_loop"
        ]
      },
      "TaskStatus": {
        "type": "string",
        "description": "Lifecycle: submitted → accepted → delivered, with rejected reachable from any open state. Transitions are forward-only; delivered and rejected are final. The retired values under_review and in_progress are no longer emitted (v1.8.0) but are still accepted on PATCH, folded onto submitted and accepted respectively.",
        "enum": [
          "submitted",
          "accepted",
          "delivered",
          "rejected"
        ]
      },
      "TaskRequest": {
        "type": "object",
        "required": [
          "task_type",
          "description"
        ],
        "properties": {
          "task_type": {
            "$ref": "#/components/schemas/TaskType"
          },
          "description": {
            "type": "string",
            "minLength": 10,
            "maxLength": 5000,
            "description": "What to do, where, and what success looks like. Be specific and self-contained."
          },
          "location_required": {
            "type": "boolean",
            "default": false
          },
          "location_detail": {
            "type": "string",
            "maxLength": 500,
            "description": "City / address / area, if location_required."
          },
          "deadline": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 datetime."
          },
          "output_format": {
            "type": "string",
            "maxLength": 200,
            "default": "text_report",
            "description": "e.g. text_report, text_report_with_photos, video, structured_json, annotated_screenshots."
          },
          "budget_usd": {
            "type": "number",
            "minimum": 0,
            "maximum": 100000,
            "deprecated": true,
            "description": "Deprecated — services are free during the pilot; accepted and ignored."
          },
          "contact_email": {
            "type": "string",
            "format": "email",
            "description": "Required unless delivery is 'status_poll'. Where results and questions are sent. Must be a real, reachable mailbox: placeholder/reserved domains (example.com etc.) are rejected and the domain must publish MX records."
          },
          "requester": {
            "type": "string",
            "maxLength": 200,
            "description": "Agent name, company, or system identifier."
          },
          "delivery": {
            "type": "string",
            "enum": [
              "email",
              "status_poll"
            ],
            "default": "email",
            "description": "How the deliverable reaches you. 'status_poll' is the no-mailbox path for autonomous agents: the result arrives as text in operator_notes via GET /api/v1/tasks/{task_id} — the task_id is the only key. Budget: 1 status_poll task per client per day."
          }
        }
      },
      "TaskSubmissionResponse": {
        "type": "object",
        "properties": {
          "task_id": {
            "type": "string",
            "example": "HFAI-2026-97BBAB04C15D2E63"
          },
          "status": {
            "$ref": "#/components/schemas/TaskStatus"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "status_url": {
            "type": "string",
            "example": "/api/v1/tasks/HFAI-2026-97BBAB04C15D2E63"
          },
          "status_page": {
            "type": "string",
            "example": "/tasks?id=HFAI-2026-97BBAB04C15D2E63"
          },
          "message": {
            "type": "string"
          }
        }
      },
      "Task": {
        "type": "object",
        "properties": {
          "task_id": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/TaskStatus"
          },
          "task_type": {
            "$ref": "#/components/schemas/TaskType"
          },
          "description": {
            "type": "string"
          },
          "location_required": {
            "type": "boolean"
          },
          "location_detail": {
            "type": "string",
            "nullable": true
          },
          "deadline": {
            "type": "string",
            "nullable": true
          },
          "output_format": {
            "type": "string"
          },
          "budget_usd": {
            "type": "number",
            "deprecated": true
          },
          "requester": {
            "type": "string"
          },
          "source": {
            "type": "string",
            "enum": [
              "api",
              "web_form"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "status_history": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "status": {
                  "$ref": "#/components/schemas/TaskStatus"
                },
                "at": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            }
          },
          "operator_notes": {
            "type": "string",
            "nullable": true,
            "description": "Notes from the human operator, when present. For delivery: status_poll tasks this is also where the deliverable arrives, and it is the exact text covered by deliverable_sha256."
          },
          "receipt": {
            "type": "string",
            "nullable": true,
            "description": "Signed deliverable receipt, present once status is delivered. Compact JWS (EdDSA/Ed25519) binding deliverable_sha256 and the lifecycle timestamps to this task_id. Verify offline against the public key at /.well-known/jwks.json — no account or callback needed. Proves integrity and origin; the timestamps inside are the service's own signed assertion, not third-party attested."
          },
          "deliverable_sha256": {
            "type": "string",
            "nullable": true,
            "description": "sha256 (hex) of the UTF-8 bytes of operator_notes as delivered. Recompute it locally to confirm the deliverable was not altered."
          },
          "receipt_issued_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the receipt was signed. If the operator later edits the deliverable, the receipt is re-issued and this timestamp changes."
          }
        }
      },
      "MessageRequest": {
        "type": "object",
        "required": [
          "message",
          "reply_to"
        ],
        "properties": {
          "message": {
            "type": "string",
            "minLength": 5,
            "maxLength": 5000
          },
          "reply_to": {
            "type": "string",
            "description": "REQUIRED. An email address (real, reachable mailbox — placeholder domains rejected, MX-checked), OR an https URL: operator replies are then pushed there as a signed webhook (HMAC-SHA256 over '<X-HumanForAI-Timestamp>.<raw body>', keyed on the thread's access_token, in X-HumanForAI-Signature as 'sha256=<hex>'). Webhook URLs must be https on the default port with a public DNS hostname; redirects are not followed. Either way the reply is also readable in the message thread."
          },
          "from": {
            "type": "string",
            "maxLength": 200,
            "description": "Agent name, company, or system identifier."
          },
          "subject": {
            "type": "string",
            "maxLength": 200
          }
        }
      },
      "MessageResponse": {
        "type": "object",
        "properties": {
          "message_id": {
            "type": "string",
            "example": "MSG-2026-1A2B3C4D"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "thread_url": {
            "type": "string",
            "example": "/api/v1/messages/MSG-2026-1A2B3C4D",
            "description": "Every message is a thread: GET this URL with the access_token to read the operator's reply without a mailbox; POST to it to follow up."
          },
          "access_token": {
            "type": "string",
            "description": "The only key to the thread. Shown once, never derivable from the message_id — persist it, and hand it (with the thread_url) to whatever process continues your task."
          },
          "message": {
            "type": "string"
          }
        }
      },
      "MessageThread": {
        "type": "object",
        "description": "A message thread as its owner sees it. status is 'received' until the operator replies, then 'answered'. An empty replies list means not yet — poll occasionally, not in a loop.",
        "properties": {
          "message_id": { "type": "string" },
          "status": { "type": "string", "enum": ["received", "answered"] },
          "created_at": { "type": "string", "format": "date-time" },
          "from": { "type": "string" },
          "subject": { "type": ["string", "null"] },
          "message": { "type": "string", "description": "The original message text." },
          "replies": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "author": { "type": "string", "enum": ["operator", "requester"] },
                "message": { "type": "string" },
                "created_at": { "type": "string", "format": "date-time" }
              }
            }
          },
          "reply_count": { "type": "integer" },
          "last_reply_at": { "type": ["string", "null"], "format": "date-time" },
          "reply_channel": { "type": "string", "enum": ["email", "webhook", "thread_only"] },
          "webhook_last_delivery": {
            "type": "object",
            "description": "Present for webhook threads after the first push attempt: at, event, HTTP status, ok, and an error string on failure.",
            "additionalProperties": true
          },
          "note": { "type": "string" }
        }
      },
      "ValidationError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "example": "validation_failed"
          },
          "details": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        }
      }
    }
  }
}