{
  "openapi": "3.0.3",
  "info": {
    "title": "GigNGo Public API",
    "description": "Public REST API for discovering local service workers, browsing open tasks, and tracking what happens to them. Most endpoints need no authentication. The attribution endpoints, the webhook registry and the `source` field on the provider feed require a partner key (`X-Api-Key`) — they carry acquisition data that is not public. See docs/FULFILLMENT_API.md.",
    "version": "1.1.0",
    "contact": {
      "name": "GigNGo",
      "url": "https://gigngo.org",
      "email": "support@gigngo.org"
    },
    "license": {
      "name": "Public API"
    }
  },
  "servers": [
    {
      "url": "https://gigngo.org/api/v1",
      "description": "Production"
    }
  ],
  "paths": {
    "/platform/info": {
      "get": {
        "operationId": "getPlatformInfo",
        "summary": "Get platform overview",
        "description": "Returns platform metadata including coverage stats, app links, and API documentation URLs.",
        "tags": [
          "Platform"
        ],
        "responses": {
          "200": {
            "description": "Platform info",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlatformInfoResponse"
                },
                "example": {
                  "ok": true,
                  "data": {
                    "name": "GigNGo",
                    "description": "Local services marketplace connecting consumers with workers",
                    "website": "https://gigngo.org",
                    "serviceCategories": 30,
                    "coverageStates": 50,
                    "servicePages": 37000,
                    "apps": {
                      "ios": "https://apps.apple.com/app/gigngo/id6462208698",
                      "android": "https://play.google.com/store/apps/details?id=org.gigngo"
                    },
                    "apiDocs": "https://gigngo.org/api/docs",
                    "llmsTxt": "https://gigngo.org/llms.txt"
                  },
                  "meta": {
                    "version": "1.0",
                    "cached": true
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/services": {
      "get": {
        "operationId": "listServices",
        "summary": "List all service categories",
        "description": "Returns all 30 service categories with slugs, display names, and URLs.",
        "tags": [
          "Services"
        ],
        "responses": {
          "200": {
            "description": "Service list",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceListResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/services/availability": {
      "get": {
        "operationId": "getServiceAvailability",
        "summary": "Check service availability by location",
        "description": "Returns worker counts per service category for a given state and optional city.",
        "tags": [
          "Services"
        ],
        "parameters": [
          {
            "name": "state",
            "in": "query",
            "description": "State slug (e.g., 'california', 'new-york')",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "city",
            "in": "query",
            "description": "City slug (e.g., 'los-angeles', 'new-york-city')",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Service availability",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceAvailabilityResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/workers/search": {
      "get": {
        "operationId": "searchWorkers",
        "summary": "Search for workers",
        "description": "Search for workers by service type and location. Returns paginated results sorted by rating.",
        "tags": [
          "Workers"
        ],
        "parameters": [
          {
            "name": "service",
            "in": "query",
            "description": "Service slug (e.g., 'handyman', 'plumber')",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "state",
            "in": "query",
            "description": "State slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "city",
            "in": "query",
            "description": "City slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Results per page (1-50, default 20)",
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 1,
              "maximum": 50
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number (default 1)",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Worker search results",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkerSearchResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/workers/{slug}": {
      "get": {
        "operationId": "getWorkerProfile",
        "summary": "Get a worker's public profile",
        "description": "Returns full public profile for a specific worker by their profile slug.",
        "tags": [
          "Workers"
        ],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Worker profile slug",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Worker profile",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkerProfileResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/tasks/open": {
      "get": {
        "operationId": "getOpenTasks",
        "summary": "Browse open tasks",
        "description": "Returns currently open tasks (work requests) that workers can apply to. Filterable by category and state.",
        "tags": [
          "Tasks"
        ],
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "description": "Category. Accepts the stored id (houseCleaning), the service slug (house-cleaning), an alias (cleaning) or the display name. All spellings match the same jobs.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "state",
            "in": "query",
            "description": "State slug or two-letter code. \"new-york\" and \"NY\" both match.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Results per page (1-50, default 20)",
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 1,
              "maximum": 50
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number (default 1)",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            }
          },
          {
            "name": "city",
            "in": "query",
            "description": "City slug or name. \"Los Angeles\", \"los-angeles\" and \"Los Angeles, CA\" all match.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "newest | oldest | most_applicants | fewest_applicants | priority. Default newest.",
            "schema": {
              "type": "string",
              "enum": [
                "newest",
                "oldest",
                "most_applicants",
                "fewest_applicants",
                "priority"
              ],
              "default": "newest"
            }
          },
          {
            "name": "expired",
            "in": "query",
            "description": "true returns only expired jobs, false drops them. Omitted returns both (the default is unchanged); meta.expiredIncluded always reports how many are on the board.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "unanswered",
            "in": "query",
            "description": "true returns only jobs with zero applicants — the fulfillment queue.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "maxAgeDays",
            "in": "query",
            "description": "Drop jobs older than this.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "minAgeDays",
            "in": "query",
            "description": "Drop jobs newer than this.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "minApplicants",
            "in": "query",
            "description": "Minimum applicant count.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Open tasks",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OpenTasksResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/categories": {
      "get": {
        "operationId": "listCategories",
        "summary": "The canonical service taxonomy",
        "tags": [
          "Services"
        ],
        "description": "Every category a job can carry, with the slug the /services pages are built from. Generated from the single place the taxonomy is authored, so it cannot drift from what the app writes.",
        "parameters": [
          {
            "name": "hasServicePage",
            "in": "query",
            "description": "Only categories we publish a service page for.",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The taxonomy",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CategoryListResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/tasks/{id}": {
      "get": {
        "operationId": "getTask",
        "summary": "One task by id",
        "tags": [
          "Tasks"
        ],
        "description": "An expired-but-posted job is returned carrying `expired: true` rather than 404 — the same job /tasks/open returns.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The task",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/tasks/outcomes": {
      "get": {
        "operationId": "getTaskOutcomes",
        "summary": "What happened to jobs",
        "tags": [
          "Tasks"
        ],
        "description": "Two modes. `ids=` asks what became of jobs you are already tracking and is exact. Without it, a feed of everything whose status changed since `since`, ordered by the transition — so a job posted in June and completed today appears in a \"since yesterday\" poll.",
        "parameters": [
          {
            "name": "ids",
            "in": "query",
            "description": "Up to 50 comma-separated task ids. Exact mode; every other filter is ignored.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "since",
            "in": "query",
            "description": "ISO timestamp, or a plain number of days. Default 30.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Comma-separated: open, assigned, in_progress, completed, captured, cancelled.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "city",
            "in": "query",
            "description": "City slug or name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "state",
            "in": "query",
            "description": "State slug or two-letter code.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "1-100, default 50.",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Default 1.",
            "schema": {
              "type": "integer",
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Outcomes",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskOutcomesResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/tasks/posting-rate": {
      "get": {
        "operationId": "getPostingRate",
        "summary": "Per-city posting history",
        "tags": [
          "Tasks"
        ],
        "description": "How often each market posts, and when. Served from a cache recomputed daily.",
        "parameters": [
          {
            "name": "state",
            "in": "query",
            "description": "State slug or two-letter code.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "city",
            "in": "query",
            "description": "City slug or name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "minPosts",
            "in": "query",
            "description": "Drop markets below this many posts in the window.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "daily",
            "in": "query",
            "description": "Include the per-day series and the platform-wide daily totals.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "1-400, default 50.",
            "schema": {
              "type": "integer",
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Posting rates",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PostingRateResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/providers/recent": {
      "get": {
        "operationId": "getRecentProviders",
        "summary": "Locals who recently joined",
        "tags": [
          "Workers"
        ],
        "description": "New provider signups. Public except for `source` and `uid`, which need a partner key — the acquisition channel is not published.",
        "security": [
          {},
          {
            "PartnerKey": []
          }
        ],
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "description": "1-365, default 30.",
            "schema": {
              "type": "integer",
              "default": 30
            }
          },
          {
            "name": "state",
            "in": "query",
            "description": "State slug or two-letter code.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "city",
            "in": "query",
            "description": "City slug or name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "trade",
            "in": "query",
            "description": "Substring match against a local’s tagged trades.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "1-100, default 50.",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Default 1.",
            "schema": {
              "type": "integer",
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Recent providers",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProvidersResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/attribution/applicants": {
      "get": {
        "operationId": "getAttributionApplicants",
        "summary": "Which channel produced each applicant",
        "tags": [
          "Attribution"
        ],
        "description": "One row per attributed application, joined to what became of it. A row exists only where the applicant arrived on a link carrying utm parameters — absence means unmeasured, never organic.",
        "security": [
          {
            "PartnerKey": []
          }
        ],
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "description": "1-365, default 30.",
            "schema": {
              "type": "integer",
              "default": 30
            }
          },
          {
            "name": "source",
            "in": "query",
            "description": "Exact utm_source.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "campaign",
            "in": "query",
            "description": "Exact utm_campaign.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "content",
            "in": "query",
            "description": "Exact utm_content.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "1-200, default 100.",
            "schema": {
              "type": "integer",
              "default": 100
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Default 1.",
            "schema": {
              "type": "integer",
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Attributed applications",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AttributionApplicantsResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/attribution/summary": {
      "get": {
        "operationId": "getAttributionSummary",
        "summary": "Applicants, hires and completions per channel",
        "tags": [
          "Attribution"
        ],
        "description": "The closed loop. Computed live rather than from daily counters, because whether an application became a hire is a fact that lands days later and a counter incremented at apply time can never learn it.",
        "security": [
          {
            "PartnerKey": []
          }
        ],
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "description": "1-365, default 30.",
            "schema": {
              "type": "integer",
              "default": 30
            }
          },
          {
            "name": "groupBy",
            "in": "query",
            "description": "source | channel | campaign | content | medium | term. Default source.",
            "schema": {
              "type": "string",
              "enum": [
                "source",
                "channel",
                "campaign",
                "content",
                "medium",
                "term"
              ],
              "default": "source"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Grouped attribution",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AttributionSummaryResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/webhooks": {
      "get": {
        "operationId": "listWebhooks",
        "summary": "List webhook subscriptions",
        "tags": [
          "Webhooks"
        ],
        "security": [
          {
            "PartnerKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Subscriptions, with signing secrets redacted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "post": {
        "operationId": "createWebhook",
        "summary": "Register a webhook",
        "tags": [
          "Webhooks"
        ],
        "description": "Deliveries are signed `X-GigNGo-Signature: sha256=<HMAC-SHA256 of the raw body>`. The signing secret is returned once, here, and never again. `task.posted` fires only once a post has cleared screening, so a job we take down is never pushed.",
        "security": [
          {
            "PartnerKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "https only, public host only."
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "task.posted",
                        "task.assigned",
                        "task.completed",
                        "task.cancelled"
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Registered. The response carries the signing secret."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      },
      "delete": {
        "operationId": "deleteWebhook",
        "summary": "Remove a webhook",
        "tags": [
          "Webhooks"
        ],
        "security": [
          {
            "PartnerKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "description": "Subscription id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted."
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/tasks/density": {
      "get": {
        "operationId": "getAreaDensity",
        "summary": "Where posted jobs actually get answered",
        "tags": [
          "Tasks"
        ],
        "description": "Per-area response rate and time-to-first-applicant. Rates are shrunk toward the platform mean before ranking, so one job with two replies does not outrank a real market; the raw counts are returned unmodified and `confidence` says how much of a row is its own evidence. Use ?minJobs=5 for the fundable shortlist.",
        "parameters": [
          {
            "name": "state",
            "in": "query",
            "description": "State slug or two-letter code.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "minJobs",
            "in": "query",
            "description": "Drop areas below this many jobs.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "heat | applicants | responseRate | adjustedResponseRate | applicantsPerJob | openUnanswered | jobs | confidence.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "1-300, default 50.",
            "schema": {
              "type": "integer",
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Area density"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "PlatformInfoResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "data": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "description": {
                "type": "string"
              },
              "website": {
                "type": "string"
              },
              "serviceCategories": {
                "type": "integer"
              },
              "coverageStates": {
                "type": "integer"
              },
              "servicePages": {
                "type": "integer"
              },
              "apps": {
                "type": "object",
                "properties": {
                  "ios": {
                    "type": "string"
                  },
                  "android": {
                    "type": "string"
                  }
                }
              },
              "apiDocs": {
                "type": "string"
              },
              "llmsTxt": {
                "type": "string"
              }
            }
          },
          "meta": {
            "type": "object"
          }
        }
      },
      "ServiceListResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "data": {
            "type": "object",
            "properties": {
              "services": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "slug": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "url": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "meta": {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer"
              }
            }
          }
        }
      },
      "ServiceAvailabilityResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "data": {
            "type": "object",
            "properties": {
              "location": {
                "type": "object",
                "properties": {
                  "state": {
                    "type": "string",
                    "nullable": true
                  },
                  "city": {
                    "type": "string",
                    "nullable": true
                  }
                }
              },
              "services": {
                "type": "object",
                "additionalProperties": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "available": {
                      "type": "boolean"
                    },
                    "workerCount": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "meta": {
            "type": "object"
          }
        }
      },
      "WorkerSearchResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "data": {
            "type": "object",
            "properties": {
              "workers": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/WorkerSummary"
                }
              }
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "WorkerProfileResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "data": {
            "$ref": "#/components/schemas/WorkerProfile"
          },
          "meta": {
            "type": "object"
          }
        }
      },
      "OpenTasksResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "data": {
            "type": "object",
            "properties": {
              "tasks": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Task"
                }
              }
            }
          },
          "meta": {
            "$ref": "#/components/schemas/PaginationMeta"
          }
        }
      },
      "WorkerSummary": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string"
          },
          "displayName": {
            "type": "string"
          },
          "skills": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "rating": {
            "type": "number",
            "nullable": true
          },
          "reviewCount": {
            "type": "integer"
          },
          "serviceArea": {
            "type": "object",
            "properties": {
              "state": {
                "type": "string",
                "nullable": true
              },
              "city": {
                "type": "string",
                "nullable": true
              },
              "radiusMiles": {
                "type": "number",
                "nullable": true
              }
            }
          },
          "badges": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "verified": {
            "type": "boolean"
          },
          "profileUrl": {
            "type": "string",
            "nullable": true
          },
          "photoUrl": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "WorkerProfile": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string"
          },
          "displayName": {
            "type": "string"
          },
          "bio": {
            "type": "string"
          },
          "skills": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "skillRates": {
            "type": "object"
          },
          "rating": {
            "type": "number",
            "nullable": true
          },
          "reviewCount": {
            "type": "integer"
          },
          "serviceArea": {
            "type": "object",
            "properties": {
              "state": {
                "type": "string",
                "nullable": true
              },
              "city": {
                "type": "string",
                "nullable": true
              },
              "radiusMiles": {
                "type": "number",
                "nullable": true
              }
            }
          },
          "availability": {
            "type": "object",
            "nullable": true
          },
          "badges": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "verified": {
            "type": "boolean"
          },
          "memberSince": {
            "type": "string",
            "nullable": true
          },
          "profileUrl": {
            "type": "string",
            "nullable": true
          },
          "photoUrl": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "Task": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "budget": {
            "type": "string",
            "nullable": true
          },
          "location": {
            "type": "object",
            "properties": {
              "state": {
                "type": "string",
                "nullable": true
              },
              "city": {
                "type": "string",
                "nullable": true
              },
              "area": {
                "type": "string",
                "nullable": true
              },
              "coords": {
                "type": "object",
                "nullable": true,
                "description": "Privacy-offset 0.3–0.8 miles from the real address, deterministic per task.",
                "properties": {
                  "lat": {
                    "type": "number"
                  },
                  "lng": {
                    "type": "number"
                  },
                  "approximate": {
                    "type": "boolean"
                  }
                }
              }
            }
          },
          "postedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "status": {
            "type": "string"
          },
          "offerCount": {
            "type": "integer",
            "description": "Deprecated alias for applicantCount."
          },
          "details": {
            "type": "string",
            "nullable": true,
            "description": "The homeowner’s \"Additional Details\" box. Often the more specific half of the post."
          },
          "categoryId": {
            "type": "string",
            "nullable": true,
            "description": "Canonical category id — the exact value stored on the job. See /categories."
          },
          "categorySlug": {
            "type": "string",
            "nullable": true,
            "description": "Canonical service slug (the /services/{state}/{city}/{slug} segment). Null when the stored category is not in the taxonomy — never silently \"other\"."
          },
          "categoryName": {
            "type": "string",
            "nullable": true
          },
          "categoryEmoji": {
            "type": "string",
            "nullable": true
          },
          "servicePageUrl": {
            "type": "string",
            "nullable": true,
            "format": "uri",
            "description": "The city service page for this job’s trade, when the city is known."
          },
          "expiresAt": {
            "type": "string",
            "nullable": true,
            "format": "date-time"
          },
          "expiresInHours": {
            "type": "number",
            "nullable": true,
            "description": "Negative once the job is past expiry."
          },
          "expired": {
            "type": "boolean",
            "description": "Derived from expiresAt at read time. 211 of 318 posted jobs are past expiry — filter with ?expired=false."
          },
          "scheduledFor": {
            "type": "string",
            "nullable": true,
            "format": "date-time",
            "description": "The date the homeowner asked for, when they gave one (61 of 318)."
          },
          "deadlineAt": {
            "type": "string",
            "nullable": true,
            "format": "date-time",
            "description": "The homeowner’s deadline, when they gave one (12 of 318)."
          },
          "renewalCount": {
            "type": "integer"
          },
          "ageHours": {
            "type": "number",
            "nullable": true
          },
          "ageDays": {
            "type": "number",
            "nullable": true
          },
          "postedAgo": {
            "type": "string",
            "nullable": true
          },
          "applicantCount": {
            "type": "integer"
          },
          "hasApplicants": {
            "type": "boolean"
          },
          "firstApplicantAt": {
            "type": "string",
            "nullable": true,
            "format": "date-time"
          },
          "hoursToFirstApplicant": {
            "type": "number",
            "nullable": true
          },
          "priority": {
            "type": "integer"
          },
          "focusTier": {
            "type": "string",
            "nullable": true
          },
          "urgency": {
            "type": "string",
            "nullable": true
          },
          "isBoosted": {
            "type": "boolean"
          },
          "imageUrl": {
            "type": "string",
            "nullable": true,
            "format": "uri"
          },
          "taskUrl": {
            "type": "string",
            "nullable": true,
            "format": "uri"
          },
          "shareableLink": {
            "type": "string",
            "nullable": true,
            "format": "uri"
          }
        }
      },
      "PaginationMeta": {
        "type": "object",
        "properties": {
          "total": {
            "type": "integer"
          },
          "page": {
            "type": "integer"
          },
          "limit": {
            "type": "integer"
          },
          "pages": {
            "type": "integer"
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "enum": [
              false
            ]
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "Category": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The value stored on a job’s `category`."
          },
          "slug": {
            "type": "string",
            "description": "The canonical service slug used by the /services pages."
          },
          "name": {
            "type": "string"
          },
          "emoji": {
            "type": "string",
            "nullable": true
          },
          "hasServicePage": {
            "type": "boolean",
            "description": "A service page template exists for this slug. Whether a given city’s page is indexable depends on local worker coverage."
          },
          "canonical": {
            "type": "boolean",
            "description": "False for an alias id that maps onto another id’s slug (e.g. \"cleaning\" → house-cleaning)."
          }
        }
      },
      "AssignedProvider": {
        "type": "object",
        "nullable": true,
        "properties": {
          "slug": {
            "type": "string",
            "description": "Pass to /workers/{slug}. Never a raw uid where a slug exists."
          },
          "displayName": {
            "type": "string"
          },
          "profileUrl": {
            "type": "string",
            "nullable": true,
            "format": "uri"
          }
        }
      },
      "TaskOutcome": {
        "type": "object",
        "description": "What happened to a job. An applicant is not a completed job — this is the endpoint that says which it was.",
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "assigned",
              "in_progress",
              "completed",
              "captured",
              "cancelled"
            ]
          },
          "previousStatus": {
            "type": "string",
            "nullable": true
          },
          "statusChangedAt": {
            "type": "string",
            "nullable": true,
            "format": "date-time",
            "description": "Server-stamped on a status transition. The feed is ordered by this."
          },
          "postedAt": {
            "type": "string",
            "nullable": true,
            "format": "date-time"
          },
          "expiresAt": {
            "type": "string",
            "nullable": true,
            "format": "date-time"
          },
          "expired": {
            "type": "boolean"
          },
          "firstApplicantAt": {
            "type": "string",
            "nullable": true,
            "format": "date-time"
          },
          "applicantCount": {
            "type": "integer"
          },
          "hoursToFirstApplicant": {
            "type": "number",
            "nullable": true
          },
          "assignedAt": {
            "type": "string",
            "nullable": true,
            "format": "date-time"
          },
          "hoursToAssign": {
            "type": "number",
            "nullable": true
          },
          "completedAt": {
            "type": "string",
            "nullable": true,
            "format": "date-time"
          },
          "hoursToComplete": {
            "type": "number",
            "nullable": true
          },
          "completedBy": {
            "type": "string",
            "nullable": true,
            "description": "\"consumer\" or \"worker\" — who closed it out."
          },
          "cancelledAt": {
            "type": "string",
            "nullable": true,
            "format": "date-time"
          },
          "assignedProvider": {
            "$ref": "#/components/schemas/AssignedProvider"
          },
          "category": {
            "type": "string",
            "nullable": true
          },
          "categoryId": {
            "type": "string",
            "nullable": true
          },
          "categorySlug": {
            "type": "string",
            "nullable": true
          },
          "categoryName": {
            "type": "string",
            "nullable": true
          },
          "location": {
            "type": "object",
            "nullable": true
          },
          "budget": {
            "type": "object",
            "nullable": true
          },
          "taskUrl": {
            "type": "string",
            "nullable": true,
            "format": "uri"
          }
        }
      },
      "Provider": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string"
          },
          "displayName": {
            "type": "string"
          },
          "joinedAt": {
            "type": "string",
            "format": "date-time"
          },
          "joinedDate": {
            "type": "string",
            "format": "date"
          },
          "location": {
            "type": "object",
            "properties": {
              "city": {
                "type": "string",
                "nullable": true
              },
              "state": {
                "type": "string",
                "nullable": true
              },
              "area": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "trades": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Often empty — a large share of new locals have not tagged a trade yet. Empty means untagged, not untrained."
          },
          "tradeSlugs": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The same trades in the canonical taxonomy."
          },
          "tradeCount": {
            "type": "integer"
          },
          "hasTrades": {
            "type": "boolean"
          },
          "profileUrl": {
            "type": "string",
            "nullable": true,
            "format": "uri"
          },
          "photoUrl": {
            "type": "string",
            "nullable": true,
            "format": "uri"
          },
          "rating": {
            "type": "number",
            "nullable": true
          },
          "reviewCount": {
            "type": "integer"
          },
          "uid": {
            "type": "string",
            "nullable": true,
            "description": "Partner key only."
          },
          "source": {
            "type": "object",
            "nullable": true,
            "description": "Acquisition channel. Partner key only — absent from the public response.",
            "properties": {
              "raw": {
                "type": "string",
                "nullable": true
              },
              "channel": {
                "type": "string",
                "nullable": true
              },
              "utmSource": {
                "type": "string",
                "nullable": true
              },
              "utmCampaign": {
                "type": "string",
                "nullable": true
              },
              "utmContent": {
                "type": "string",
                "nullable": true
              }
            }
          }
        }
      },
      "PostingRateArea": {
        "type": "object",
        "properties": {
          "area": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "citySlug": {
            "type": "string"
          },
          "state": {
            "type": "string",
            "nullable": true
          },
          "posts": {
            "type": "integer"
          },
          "postsPerWeek": {
            "type": "number",
            "description": "Over the span this market has actually existed, not over the whole window."
          },
          "postsLast30Days": {
            "type": "integer"
          },
          "postsPrevious30Days": {
            "type": "integer"
          },
          "trend30d": {
            "type": "number",
            "nullable": true,
            "description": "last30 / prev30. Null (not Infinity) for a market with no previous period."
          },
          "activeDays": {
            "type": "integer"
          },
          "firstPostAt": {
            "type": "string",
            "format": "date-time"
          },
          "lastPostAt": {
            "type": "string",
            "format": "date-time"
          },
          "daysSinceLastPost": {
            "type": "number"
          },
          "byHourUtc": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "description": "24 buckets, UTC."
          },
          "byDayOfWeek": {
            "type": "array",
            "items": {
              "type": "integer"
            },
            "description": "7 buckets, 0 = Sunday, UTC."
          },
          "topCategories": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "slug": {
                  "type": "string"
                },
                "posts": {
                  "type": "integer"
                }
              }
            }
          },
          "daily": {
            "type": "object",
            "description": "date → count. Only with ?daily=true."
          }
        }
      },
      "AttributionRow": {
        "type": "object",
        "properties": {
          "applicationId": {
            "type": "string"
          },
          "taskId": {
            "type": "string",
            "nullable": true
          },
          "appliedAt": {
            "type": "string",
            "nullable": true,
            "format": "date-time"
          },
          "source": {
            "type": "string",
            "nullable": true
          },
          "channel": {
            "type": "string",
            "nullable": true
          },
          "medium": {
            "type": "string",
            "nullable": true
          },
          "campaign": {
            "type": "string",
            "nullable": true
          },
          "content": {
            "type": "string",
            "nullable": true,
            "description": "The per-account / per-message / promo-code slot."
          },
          "term": {
            "type": "string",
            "nullable": true
          },
          "landingPage": {
            "type": "string",
            "nullable": true
          },
          "referrerHost": {
            "type": "string",
            "nullable": true
          },
          "platform": {
            "type": "string",
            "nullable": true
          },
          "clickAgeHours": {
            "type": "number",
            "nullable": true,
            "description": "How old the stored click was when the application happened."
          },
          "task": {
            "type": "object",
            "nullable": true
          },
          "outcome": {
            "type": "object",
            "properties": {
              "hired": {
                "type": "boolean",
                "description": "This applicant is the job’s acceptedWorkerId."
              },
              "completed": {
                "type": "boolean",
                "description": "Hired AND the job finished. A job finished by somebody else is not a win for this channel."
              },
              "completedAt": {
                "type": "string",
                "nullable": true,
                "format": "date-time"
              }
            }
          }
        }
      },
      "AttributionGroup": {
        "type": "object",
        "properties": {
          "applicants": {
            "type": "integer"
          },
          "hires": {
            "type": "integer"
          },
          "completions": {
            "type": "integer"
          },
          "distinctTasks": {
            "type": "integer"
          },
          "hireRate": {
            "type": "number",
            "description": "Percent, raw and unsmoothed. Read it beside `applicants`."
          },
          "completionRate": {
            "type": "number"
          },
          "firstAt": {
            "type": "string",
            "nullable": true,
            "format": "date-time"
          },
          "lastAt": {
            "type": "string",
            "nullable": true,
            "format": "date-time"
          }
        }
      },
      "CategoryListResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "data": {
            "type": "object",
            "properties": {
              "categories": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Category"
                }
              }
            }
          },
          "meta": {
            "type": "object"
          }
        }
      },
      "TaskOutcomesResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "data": {
            "type": "object",
            "properties": {
              "outcomes": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/TaskOutcome"
                }
              }
            }
          },
          "meta": {
            "type": "object"
          }
        }
      },
      "ProvidersResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "data": {
            "type": "object",
            "properties": {
              "providers": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Provider"
                }
              }
            }
          },
          "meta": {
            "type": "object"
          }
        }
      },
      "PostingRateResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "data": {
            "type": "object",
            "properties": {
              "areas": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/PostingRateArea"
                }
              }
            }
          },
          "meta": {
            "type": "object"
          }
        }
      },
      "AttributionApplicantsResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "data": {
            "type": "object",
            "properties": {
              "applicants": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/AttributionRow"
                }
              }
            }
          },
          "meta": {
            "type": "object"
          }
        }
      },
      "AttributionSummaryResponse": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "data": {
            "type": "object",
            "properties": {
              "groupBy": {
                "type": "string"
              },
              "groups": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/AttributionGroup"
                }
              }
            }
          },
          "meta": {
            "type": "object"
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid parameters",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "ok": false,
              "error": {
                "code": "BAD_REQUEST",
                "message": "Worker slug is required"
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "ok": false,
              "error": {
                "code": "NOT_FOUND",
                "message": "Worker profile not found"
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded (200 req/15min)",
        "headers": {
          "Retry-After": {
            "description": "Seconds until rate limit resets",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Limit": {
            "description": "Maximum requests per window",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Remaining requests in current window",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Reset": {
            "description": "ISO timestamp when limit resets",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "ok": false,
              "error": {
                "code": "RATE_LIMITED",
                "message": "API rate limit exceeded (200 req/15min). Check X-RateLimit-Reset header for reset time."
              }
            }
          }
        }
      },
      "InternalError": {
        "description": "Server error",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "ok": false,
              "error": {
                "code": "INTERNAL_ERROR",
                "message": "An internal error occurred"
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "A partner key is required for this endpoint.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "Forbidden": {
        "description": "The key is invalid or is not scoped for this endpoint.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      }
    },
    "securitySchemes": {
      "PartnerKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key",
        "description": "Partner key. Issued per integration and revocable without a deploy. The shared admin secret on `Authorization: Bearer` also works."
      }
    }
  },
  "tags": [
    {
      "name": "Platform",
      "description": "What GigNGo is and how matching works."
    },
    {
      "name": "Services",
      "description": "The service taxonomy and where it is covered."
    },
    {
      "name": "Workers",
      "description": "Locals — search, profiles, and who recently joined."
    },
    {
      "name": "Tasks",
      "description": "Jobs: what is open, what happened to them, and how often each market posts."
    },
    {
      "name": "Attribution",
      "description": "Which channel, account and message produced an applicant. Partner key required."
    },
    {
      "name": "Webhooks",
      "description": "Push instead of poll. Partner key required."
    }
  ]
}
