openapi: 3.1.0
info:
  title: Hey Human services and physical task API
  version: 2.0.0-preview
  description: |
    Provider discovery and service bookings are documented at /openapi-services.json.
    Both APIs share scoped agent credentials and preserve agent-only demand.
    Agent-facing API for bounded physical-world requests, worker-priced bids,
    explicit selection, and worker confirmation. This is not a live paid service.
    New requests open for bids with mode bid_collection and funding pending; hiring is locked. A server-only development test flag
    can simulate funding; actual_money_moved is always false in this build.
    Pricing and attempt terms are illustrative. Real funding, payout, geospatial
    matching, reputation measurement, automated evidence verification, owner spend-policy
    approval, durable webhooks, and MCP distribution are not connected.
    The original creative-review API remains separately documented at /openapi.yaml.
    Authenticated worker profile, push-subscription, bidding and confirmation routes are documented in docs/LIVE_TASK_CONNECTION.md.
    Open requests match opted-in workers by city, state and task type; configured Web Push uses a durable retry queue.
servers:
  - url: /
    description: Current application origin
  - url: http://localhost:3000
    description: Local Next.js development server
security:
  - AgentBearer: []
tags:
  - name: Tasks
  - name: Bids
  - name: Awards
paths:
  /api/v2/providers:
    $ref: './openapi-services.json#/paths/~1api~1v2~1providers'
  /api/v2/providers/{id}:
    $ref: './openapi-services.json#/paths/~1api~1v2~1providers~1{id}'
  /api/v2/providers/{id}/availability:
    $ref: './openapi-services.json#/paths/~1api~1v2~1providers~1{id}~1availability'
  /api/v2/bookings:
    $ref: './openapi-services.json#/paths/~1api~1v2~1bookings'
  /api/v2/bookings/{id}:
    $ref: './openapi-services.json#/paths/~1api~1v2~1bookings~1{id}'
  /api/v2/bookings/{id}/accept:
    $ref: './openapi-services.json#/paths/~1api~1v2~1bookings~1{id}~1accept'
  /api/v2/bookings/{id}/cancellation:
    $ref: './openapi-services.json#/paths/~1api~1v2~1bookings~1{id}~1cancellation'
  /api/v2/tasks:
    post:
      tags: [Tasks]
      operationId: createPhysicalTask
      summary: Create a bounded request with an all-in maximum
      description: |
        Requires tasks:write. The cap includes worker pay and all applicable fees.
        Unknown fields, including client-supplied funding flags, are rejected.
        Ordered deadlines must be in the future for bidding, with resolution
        within 14 days. Creation may notify matching workers to bid; it does not hire or send anyone to do field work.
        Default and production behavior is open_for_bids with pending funding; alerts and bids are enabled, but awards return funding_required (402). No money moves.
      x-required-scope: tasks:write
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTask'
      responses:
        '201':
          description: New request created; meta.created is true
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskResponse'
        '200':
          description: Original request replayed; meta.created is false
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskResponse'
        default:
          $ref: '#/components/responses/Error'
    get:
      tags: [Tasks]
      operationId: listPhysicalTasks
      summary: List the authenticated organization's requests
      x-required-scope: tasks:read
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
      responses:
        '200':
          description: Task array; meta.count reports the returned count
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Task'
                  meta:
                    $ref: '#/components/schemas/Meta'
        default:
          $ref: '#/components/responses/Error'
  /api/v2/tasks/{id}:
    parameters:
      - $ref: '#/components/parameters/TaskId'
    get:
      tags: [Tasks]
      operationId: getPhysicalTask
      summary: Read canonical state, version, funding, and current award
      description: No completed evidence/result payload is implemented yet.
      x-required-scope: tasks:read
      responses:
        '200':
          description: Current organization-scoped task
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskResponse'
        default:
          $ref: '#/components/responses/Error'
  /api/v2/tasks/{id}/bids:
    parameters:
      - $ref: '#/components/parameters/TaskId'
    get:
      tags: [Bids]
      operationId: listPhysicalTaskBids
      summary: Inspect current bid versions and award eligibility
      description: |
        Requires bids:read. Returns latest versions, including withdrawn or
        unawardable bids with explicit reasons. Over-cap bids cannot be awarded.
        Worker reliability and distance currently return null, not measured values.
        Estimated finish means physical observation completion, before observe_by;
        uploads have a separate submit_by deadline. Estimates are not guarantees.
      x-required-scope: bids:read
      responses:
        '200':
          description: Bid versions and the task version used for a subsequent award
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: object
                    required: [task_version, bids]
                    properties:
                      task_version:
                        type: integer
                        minimum: 1
                      bids:
                        type: array
                        items:
                          $ref: '#/components/schemas/Bid'
                  meta:
                    $ref: '#/components/schemas/Meta'
        default:
          $ref: '#/components/responses/Error'
  /api/v2/tasks/{id}/bids/{bidId}/photo:
    get:
      operationId: getTaskWorkerPhoto
      summary: Read the current managed portrait of a worker who bid on this account's task
      x-required-scope: bids:read
      parameters:
        - $ref: '#/components/parameters/TaskId'
        - name: bidId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: A normalized WebP portrait; private no-store response
          content:
            image/webp:
              schema:
                type: string
                format: binary
        default:
          $ref: '#/components/responses/Error'
  /api/v2/tasks/{id}/award:
    parameters:
      - $ref: '#/components/parameters/TaskId'
    post:
      tags: [Awards]
      operationId: awardPhysicalTaskBid
      summary: Explicitly select a live bid version
      description: |
        Requires awards:write and an Idempotency-Key. Validates current task,
        bid and quote versions, spec hash, cap, funding, expiry, and worker capacity.
        Returns award_pending, not a confirmed assignment. The selected human must
        confirm through their authenticated worker interface before travel/start.
        The current build permits awards only in a server-enabled local test
        simulation. Production has no live funding path and cannot hire workers.
      x-required-scope: awards:write
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AwardInput'
      responses:
        '200':
          description: Selection recorded; inspect current_award.confirmation_by
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskResponse'
        default:
          $ref: '#/components/responses/Error'
  /api/v2/tasks/{id}/cancellation:
    parameters:
      - $ref: '#/components/parameters/TaskId'
    post:
      tags: [Tasks]
      operationId: cancelPhysicalTask
      summary: Cancel before confirmed assignment
      description: |
        Requires tasks:write. No request body is needed. Cancels pending funding,
        open bidding, or pending confirmation and releases simulated funds/holds.
        Confirmed or started work returns cancellation_requires_resolution;
        automatic postassignment compensation settlement is not implemented.
        Terminal noncanceled tasks return task_terminal. This route never moves money.
      x-required-scope: tasks:write
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Canceled task, or replay of a successful cancellation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TaskResponse'
        default:
          $ref: '#/components/responses/Error'
  /api/v2/tasks/{id}/events:
    parameters:
      - $ref: '#/components/parameters/TaskId'
    get:
      tags: [Tasks]
      operationId: getPhysicalTaskEvents
      summary: Retrieve the organization's recorded task events
      description: Polling is supported; no durable webhook delivery is configured.
      x-required-scope: tasks:read
      responses:
        '200':
          description: Recorded task, bid, award, and system events
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Event'
                  meta:
                    $ref: '#/components/schemas/Meta'
        default:
          $ref: '#/components/responses/Error'
  /api/v2/tasks/{id}/review:
    post:
      tags: [Tasks]
      operationId: reviewPhysicalTask
      summary: Accept the latest submission or request in-scope changes
      x-required-scope: tasks:write
      parameters:
        - {name: id, in: path, required: true, schema: {type: string}}
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [submission_id, decision, note]
              properties:
                submission_id: {type: string}
                decision: {type: string, enum: [accept, request_changes]}
                note: {type: string, minLength: 5, maxLength: 2000}
      responses:
        '200': {description: Updated task; acceptance does not move money}
        default:
          $ref: '#/components/responses/Error'
  /api/v2/tasks/{id}/messages:
    post:
      tags: [Tasks]
      operationId: messagePhysicalTask
      summary: Message the assigned worker without amending the agreement
      x-required-scope: tasks:write
      parameters:
        - {name: id, in: path, required: true, schema: {type: string}}
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [body]
              properties:
                body: {type: string, minLength: 1, maxLength: 2000}
      responses:
        '200': {description: Message saved on the task}
        default:
          $ref: '#/components/responses/Error'
  /api/v2/tasks/{id}/evidence:
    get:
      tags: [Tasks]
      operationId: readPhysicalTaskEvidence
      summary: Get a five-minute private URL for submitted evidence
      x-required-scope: tasks:read
      parameters:
        - {name: id, in: path, required: true, schema: {type: string}}
        - {name: id, in: query, required: true, description: Evidence reference, schema: {type: string}}
      responses:
        '200': {description: Signed URL and expires_in; never persist the URL}
        default:
          $ref: '#/components/responses/Error'

components:
  securitySchemes:
    AgentBearer:
      type: http
      scheme: bearer
      description: |
        Organization/environment-scoped agent API key. Required scopes are
        documented by x-required-scope. hh_test_demo works only outside production.
        It is not a worker credential or customer-spending approval mechanism.
  parameters:
    TaskId:
      name: id
      in: path
      required: true
      schema:
        type: string
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: |
        Keep the same key and identical payload to retry one operation. A reused
        key with changed input returns idempotency_conflict. Mutations are scoped
        to the authenticated operation; use a new key for a distinct intended action.
      schema:
        type: string
        minLength: 8
        maxLength: 128
        pattern: '^[A-Za-z0-9._:-]{8,128}$'
  responses:
    Error:
      description: |
        Machine-readable failure. 400 includes invalid_json or missing_idempotency_key;
        401 unauthorized; 403 insufficient_scope; 404 not_found; 413 payload_too_large
        for JSON bodies above 32 KB; 422 invalid_request, invalid_query,
        invalid_deadlines, or invalid_estimate; 402 funding_required; 409 includes
        idempotency_conflict, stale_version, task_not_open, bid_superseded,
        bid_unavailable, bid_over_cap, quote_changed, quote_expired,
        worker_unavailable, confirmation_expired, task_terminal, and
        cancellation_requires_resolution. 503 task_service_unavailable indicates
        a service failure, not funding or assignment. X-Request-Id matches meta.request_id.
      headers:
        X-Request-Id:
          schema:
            type: string
      content:
        application/json:
          schema:
            type: object
            required: [error, meta]
            properties:
              error:
                type: object
                required: [code, message]
                properties:
                  code:
                    type: string
                  message:
                    type: string
                  details: {}
              meta:
                $ref: '#/components/schemas/Meta'
  schemas:
    Meta:
      type: object
      required: [request_id]
      properties:
        request_id:
          type: string
        count:
          type: integer
          minimum: 0
        created:
          type: boolean
    MoneyCents:
      type: integer
      minimum: 1
      maximum: 1000000
      description: Integer USD minor units; never a floating-point dollar amount
    Coordinates:
      type: object
      additionalProperties: false
      required: [latitude, longitude]
      description: Supplied work location in WGS84 decimal degrees. Omit when unknown.
      properties:
        latitude: { type: number, minimum: -90, maximum: 90 }
        longitude: { type: number, minimum: -180, maximum: 180 }
    Place:
      type: object
      additionalProperties: false
      required: [store_name, address, city, region, timezone]
      properties:
        store_name:
          type: string
          minLength: 2
          maxLength: 160
        address:
          type: string
          minLength: 5
          maxLength: 300
        city:
          type: string
          minLength: 2
          maxLength: 100
        region:
          type: string
          minLength: 2
          maxLength: 100
        postal_code:
          type: string
          minLength: 3
          maxLength: 20
        coordinates:
          $ref: '#/components/schemas/Coordinates'
        timezone:
          type: string
          minLength: 3
          maxLength: 100
          description: Valid IANA time zone, such as America/New_York
    Item:
      type: object
      additionalProperties: false
      required: [name, sku]
      properties:
        name:
          type: string
          minLength: 2
          maxLength: 200
        sku:
          type: string
          minLength: 1
          maxLength: 128
        variant:
          type: string
          maxLength: 200
    Deadlines:
      type: object
      additionalProperties: false
      required: [bid_by, dispatch_by, observe_by, submit_by, result_by]
      description: |
        RFC 3339 offset-aware instants. Require bid_by < dispatch_by <= observe_by
        <= submit_by <= result_by. Bidding must close in the future and result_by
        must fall within 14 days of creation. Allow feasible travel and review time.
      properties:
        bid_by:
          type: string
          format: date-time
        dispatch_by:
          type: string
          format: date-time
        observe_by:
          type: string
          format: date-time
        submit_by:
          type: string
          format: date-time
        result_by:
          type: string
          format: date-time
    Budget:
      type: object
      additionalProperties: false
      required: [all_in_max_cents, currency]
      properties:
        all_in_max_cents:
          $ref: '#/components/schemas/MoneyCents'
        currency:
          type: string
          const: USD
    CreateTask:
      oneOf:
        - $ref: '#/components/schemas/RetailTask'
        - $ref: '#/components/schemas/GeneralTask'
      discriminator:
        propertyName: category
    RetailTask:
      type: object
      additionalProperties: false
      required: [category, place, item, budget, deadlines, customer_reference]
      properties:
        category:
          type: string
          const: retail_item_check.v1
        place:
          $ref: '#/components/schemas/Place'
        item:
          $ref: '#/components/schemas/Item'
        budget:
          $ref: '#/components/schemas/Budget'
        deadlines:
          $ref: '#/components/schemas/Deadlines'
        customer_reference:
          type: string
          minLength: 1
          maxLength: 128
    GeneralTask:
      type: object
      additionalProperties: false
      required: [category, family, title, brief, place, requirements, checklist, evidence, max_effort_minutes, budget, deadlines, customer_reference]
      properties:
        category:
          const: physical_task.v1
        family:
          enum: [observation, photo_video, attendance, practical_help, custom]
        title:
          type: string
          minLength: 5
          maxLength: 160
        brief:
          type: string
          minLength: 20
          maxLength: 3000
        place:
          type: object
          additionalProperties: false
          required: [name, address, city, region, timezone]
          properties:
            name: { type: string, minLength: 2, maxLength: 160 }
            address: { type: string, minLength: 5, maxLength: 300 }
            city: { type: string, minLength: 2, maxLength: 100 }
            region: { type: string, minLength: 2, maxLength: 100 }
            postal_code: { type: string, minLength: 3, maxLength: 20 }
            coordinates: { $ref: '#/components/schemas/Coordinates' }
            timezone: { type: string, description: Valid IANA timezone }
        requirements:
          type: array
          maxItems: 10
          items: { type: string, minLength: 2, maxLength: 160 }
        checklist:
          type: array
          minItems: 1
          maxItems: 12
          items: { type: string, minLength: 5, maxLength: 300 }
        evidence:
          type: array
          minItems: 1
          maxItems: 4
          items:
            enum: [written_report, photo, video, measurement]
        max_effort_minutes: { type: integer, minimum: 5, maximum: 240 }
        budget:
          $ref: '#/components/schemas/Budget'
        deadlines:
          type: object
          additionalProperties: false
          required: [bid_by, dispatch_by, complete_by, submit_by, result_by]
          description: Ordered instants; bid_by must be strictly before dispatch_by. Returned specs also expose observe_by as the scheduling alias for complete_by.
          properties:
            bid_by: { type: string, format: date-time }
            dispatch_by: { type: string, format: date-time }
            complete_by: { type: string, format: date-time }
            submit_by: { type: string, format: date-time }
            result_by: { type: string, format: date-time }
        customer_reference: { type: string, minLength: 1, maxLength: 128 }
    Spec:
      description: General task specs retain place.store_name as the location-name alias and add title, family, requirements, evidence and max_effort_minutes. Item is present only for retail checks.
      type: object
      required: [category, place, deadlines, checklist, scope]
      properties:
        category:
          type: string
          enum: [retail_item_check.v1, physical_task.v1]
        place:
          $ref: '#/components/schemas/Place'
        item:
          $ref: '#/components/schemas/Item'
        deadlines:
          $ref: '#/components/schemas/Deadlines'
        checklist:
          type: array
          items:
            type: string
        scope:
          type: string
    Task:
      type: object
      required: [id, object, status, version, mode, spec, spec_hash, customer_reference, budget, funding, pricing, current_award, created_at, updated_at]
      properties:
        id:
          type: string
        object:
          type: string
          const: physical_task
        status:
          type: string
          enum: [pending_funding, open_for_bids, award_pending, assigned, in_progress, submitted, reviewing, completed, unfulfilled, canceled, expired]
          description: Submission/review/completion states are reserved; no field evidence pipeline ships in this preview.
        version:
          type: integer
          minimum: 1
        mode:
          type: string
          enum: [development_simulation, bid_collection, unfunded_preview]
        spec:
          $ref: '#/components/schemas/Spec'
        spec_hash:
          type: string
        customer_reference:
          type: string
        budget:
          $ref: '#/components/schemas/Budget'
        funding:
          type: object
          required: [status, reserved_amount_cents, actual_money_moved]
          properties:
            status:
              type: string
              enum: [pending, simulated_authorized, simulated_partially_released, simulated_released]
            reserved_amount_cents:
              type: integer
              minimum: 0
            actual_money_moved:
              type: boolean
              const: false
        pricing:
          type: object
          required: [policy, illustrative, platform_fee_cents]
          properties:
            policy:
              type: string
              const: illustrative-usd-v1
            illustrative:
              type: boolean
              const: true
            platform_fee_cents:
              type: integer
              const: 100
        current_award:
          oneOf:
            - $ref: '#/components/schemas/Award'
            - type: "null"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    TaskResponse:
      type: object
      required: [data, meta]
      properties:
        data:
          $ref: '#/components/schemas/Task'
        meta:
          $ref: '#/components/schemas/Meta'
    BidTerms:
      type: object
      required: [policy_version, valid_observation_worker_pay_cents, documented_attempt_worker_pay_cents, before_assignment_customer_charge_cents, before_assignment_worker_pay_cents, after_assignment]
      description: Illustrative simulation terms, not a complete live compensation schedule
      properties:
        policy_version:
          type: string
          const: illustrative-attempt-cancellation-v1
        valid_observation_worker_pay_cents:
          $ref: '#/components/schemas/MoneyCents'
        documented_attempt_worker_pay_cents:
          $ref: '#/components/schemas/MoneyCents'
        before_assignment_customer_charge_cents:
          type: integer
          const: 0
        before_assignment_worker_pay_cents:
          type: integer
          const: 0
        after_assignment:
          type: string
          const: operator_resolution_required
    BidQuote:
      type: object
      required: [version, quote_id, spec_hash, worker_pay_cents, platform_fee_cents, other_charges_cents, customer_total_cents, currency, estimated_start_at, estimated_finish_at, bid_expires_at, quote_expires_at, note, terms, created_at]
      properties:
        version:
          type: integer
          minimum: 1
        quote_id:
          type: string
        spec_hash:
          type: string
        worker_pay_cents:
          $ref: '#/components/schemas/MoneyCents'
        platform_fee_cents:
          type: integer
          const: 100
        other_charges_cents:
          type: integer
          const: 0
        customer_total_cents:
          type: integer
          minimum: 1
        currency:
          type: string
          const: USD
        estimated_start_at:
          type: string
          format: date-time
        estimated_finish_at:
          type: string
          format: date-time
          description: Estimated physical observation completion; must fit observe_by
        bid_expires_at:
          type: string
          format: date-time
        quote_expires_at:
          type: string
          format: date-time
        note:
          type: [string, "null"]
        terms:
          $ref: '#/components/schemas/BidTerms'
        created_at:
          type: string
          format: date-time
    Bid:
      allOf:
        - $ref: '#/components/schemas/BidQuote'
        - type: object
          required: [id, status, worker, within_budget, awardable, unavailable_reasons]
          properties:
            id:
              type: string
            status:
              type: string
              enum: [active, withdrawn]
            worker:
              type: object
              required: [id, name, photo_url, self_reported_skills, reliability, distance_miles]
              properties:
                id:
                  type: string
                name:
                  type: string
                photo_url:
                  type: [string, "null"]
                  description: Managed worker portrait. Requires bids:read; null only for ineligible legacy bids missing a photo.
                self_reported_skills:
                  type: array
                  description: Worker-entered skills; not verified credentials.
                  items:
                    type: string
                reliability:
                  type: "null"
                  description: Not measured by this build
                distance_miles:
                  type: "null"
                  description: No geospatial calculation connected
            within_budget:
              type: boolean
            awardable:
              type: boolean
            unavailable_reasons:
              type: array
              items:
                type: string
    AwardInput:
      type: object
      additionalProperties: false
      required: [task_version, bid_id, bid_version, quote_id, spec_hash]
      properties:
        task_version:
          type: integer
          minimum: 1
        bid_id:
          type: string
          minLength: 1
          maxLength: 100
        bid_version:
          type: integer
          minimum: 1
        quote_id:
          type: string
          minLength: 1
          maxLength: 100
        spec_hash:
          type: string
          minLength: 1
          maxLength: 100
    Award:
      type: object
      required: [id, task_id, worker_id, bid_id, bid_version, quote, status, confirmation_by, confirmed_at, started_at, created_at, updated_at]
      properties:
        id:
          type: string
        task_id:
          type: string
        worker_id:
          type: string
        bid_id:
          type: string
        bid_version:
          type: integer
        quote:
          $ref: '#/components/schemas/BidQuote'
        status:
          type: string
          enum: [pending_confirmation, confirmed, declined, expired, canceled]
        confirmation_by:
          type: string
          format: date-time
        confirmed_at:
          type: [string, "null"]
          format: date-time
        started_at:
          type: [string, "null"]
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    Event:
      type: object
      required: [id, task_id, sequence, type, actor, occurred_at, data]
      properties:
        id:
          type: string
        task_id:
          type: string
        sequence:
          type: integer
          minimum: 1
        type:
          type: string
        actor:
          type: string
          enum: [agent, worker, system]
        occurred_at:
          type: string
          format: date-time
        data:
          type: object
          additionalProperties: true
