{
  "openapi": "3.0.3",
  "info": {
    "title": "RoleDawn APIs: Jobs, App Reviews, Hiring Signals",
    "version": "1.0.0",
    "description": "3 public APIs on one host. Full guides: https://roledawn.com/api (Jobs), https://roledawn.com/api/app-reviews (App Reviews), https://roledawn.com/guides/clay-http-api-hiring-signals (Hiring Signals).\n\n## Jobs API\n\nOne GET request returns all open jobs on a company's public Greenhouse, Lever or Ashby job board, normalised to the same fields for all three ATS: title, location, department, remote flag, job URL and posting date.\n\nThe board is read live from the ATS's public job-board endpoint (no login), after checking the host's robots.txt, with per-domain rate limits. Each board is cached at the edge for up to 15 minutes, so repeated and paged requests are fast.\n\nDirect calls to roledawn.com are a free demo (no API key, 50 requests per IP per day, 60 per minute). For production volume, subscribe on RapidAPI: https://rapidapi.com/PENGCHENGLI/api/ats-jobs-api-greenhouse-lever-ashby-job-listings\n\nNot affiliated with Greenhouse, Lever or Ashby. Documentation: https://roledawn.com/api\n\n## App Reviews API\n\nApp Store and Google Play app data with one GET request, in the same JSON fields for both stores.\n\n- GET /api/v1/app: app metadata: rating, ratings count, star histogram (both stores), version, release notes, price, installs (Google Play), size and minimum iOS (App Store).\n- GET /api/v1/app-reviews: the reviews shown on the public store page (about 10 per country on the App Store, about 20 per country and language on Google Play) with star filter, sorting and paging; Google Play reviews include the app version and the developer's reply.\n\nData is read live from public sources whose robots.txt allows it (the iTunes Search API lookup, the public App Store and Google Play app pages), at most 1 request per second per store, and cached at the edge for up to 1 hour.\n\nDirect calls to roledawn.com are a free demo (no API key, 50 requests per IP per day, 60 per minute). For production volume, subscribe on RapidAPI: https://rapidapi.com/PENGCHENGLI/api/app-store-google-play-reviews-api\n\nNot affiliated with Apple or Google. Documentation: https://roledawn.com/api/app-reviews\n\n## Hiring Signals API\n\nOne GET request returns the companies that just started hiring in a role family (first_hire: the family is new on their own careers page, e.g. their first SDR or RevOps role) or whose hiring in it jumped (surge), with the public job post behind every signal.\n\nRoleDawn re-reads company career pages on Greenhouse, Lever, Ashby, Workable, Recruitee and Workday about once a day and sorts every title into a role family. Staffing agencies and large established companies are left out; one row per company, posts that say \"founding\" or \"first\" come first, then signals observed on a board already tracked, then the newest.\n\nFree tier: up to 20 companies per query (the top 20, the same as the public Weekly List sample at https://roledawn.com/weekly-list/sample). No API key is needed to start: 50 requests per IP per day and 60 per minute, and repeat queries served from the edge cache (X-Cache: HIT) do not count. Callers on shared IPs (Clay HTTP API columns, hosted AI agents) should send a free personal key in the X-RoleDawn-Key header (email only, shown right away at https://roledawn.com/api/key): 50 requests per key per day, counted per key instead of per IP. Optional X-Client header names your integration for usage stats. Guide for Clay's HTTP API column: https://roledawn.com/guides/clay-http-api-hiring-signals",
    "contact": {
      "name": "RoleDawn",
      "url": "https://roledawn.com/contact"
    },
    "termsOfService": "https://roledawn.com/terms"
  },
  "externalDocs": {
    "description": "Guide with real example requests and responses",
    "url": "https://roledawn.com/api"
  },
  "servers": [
    {
      "url": "https://roledawn.com",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Jobs",
      "description": "Open jobs on a company's ATS job board"
    },
    {
      "name": "Apps",
      "description": "App Store and Google Play app metadata and reviews"
    },
    {
      "name": "Signals",
      "description": "Companies that just started hiring a new team"
    }
  ],
  "paths": {
    "/api/v1/jobs": {
      "get": {
        "tags": [
          "Jobs"
        ],
        "operationId": "listJobs",
        "summary": "List open jobs on a company's Greenhouse, Lever or Ashby job board",
        "description": "Returns the open jobs on one company's public job board, newest first. Filters (remote, q, location) and paging (limit, offset) are applied to the cached board, so paging through a large board costs no extra upstream requests.",
        "parameters": [
          {
            "name": "ats",
            "in": "query",
            "required": true,
            "description": "Applicant tracking system that hosts the job board.",
            "schema": {
              "type": "string",
              "enum": [
                "greenhouse",
                "lever",
                "ashby"
              ]
            },
            "example": "greenhouse"
          },
          {
            "name": "company",
            "in": "query",
            "required": true,
            "description": "The company's job-board slug, as it appears in the careers URL: boards.greenhouse.io/<slug> or job-boards.greenhouse.io/<slug>, jobs.lever.co/<slug>, jobs.ashbyhq.com/<slug>. Greenhouse and Lever slugs are case-insensitive.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$"
            },
            "example": "stripe"
          },
          {
            "name": "remote",
            "in": "query",
            "required": false,
            "description": "true = only remote jobs, false = only non-remote jobs. Omit for all.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Case-insensitive text that must appear in the job title (max 100 characters).",
            "schema": {
              "type": "string",
              "maxLength": 100
            },
            "example": "engineer"
          },
          {
            "name": "location",
            "in": "query",
            "required": false,
            "description": "Case-insensitive text that must appear in the location (max 100 characters), e.g. \"London\" or \"Remote\".",
            "schema": {
              "type": "string",
              "maxLength": 100
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Jobs per page.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of matching jobs to skip. Use next_offset from the previous page.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 100000,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The company's open jobs (one page). Example: a real response for ats=greenhouse&company=stripe&limit=1, captured 2026-10-07.",
            "headers": {
              "X-Cache": {
                "description": "HIT when the board was served from the edge cache, MISS when it was read from the ATS just now.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "HIT",
                    "MISS"
                  ]
                }
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobsResponse"
                },
                "example": {
                  "ats": "greenhouse",
                  "company": "stripe",
                  "company_name": "Stripe",
                  "board_url": "https://job-boards.greenhouse.io/stripe",
                  "fetched_at": "2026-10-07T09:32:29.178Z",
                  "total": 733,
                  "count": 1,
                  "limit": 1,
                  "offset": 0,
                  "next_offset": 1,
                  "jobs": [
                    {
                      "id": "8241345",
                      "title": "Account Executive, Velocity Scaled Grower (Italian or Spanish fluency)",
                      "company": "stripe",
                      "company_name": "Stripe",
                      "ats": "greenhouse",
                      "location": "Dublin",
                      "department": "1185 Account Executives (EMEA)",
                      "remote": false,
                      "url": "https://stripe.com/jobs/search?gh_jid=8241345",
                      "posted_at": "2026-10-07T09:23:16.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "$ref": "#/components/responses/UpstreamError"
          },
          "503": {
            "$ref": "#/components/responses/SourceBlocked"
          },
          "504": {
            "$ref": "#/components/responses/UpstreamTimeout"
          }
        }
      }
    },
    "/api/v1/app": {
      "get": {
        "tags": [
          "Apps"
        ],
        "operationId": "getApp",
        "summary": "Get App Store or Google Play app metadata",
        "description": "Rating, ratings count, star histogram, version, release notes, price and more for one app in one country, in the same fields for both stores.",
        "parameters": [
          {
            "name": "store",
            "in": "query",
            "required": false,
            "description": "apple (App Store) or google (Google Play). Optional: inferred from id (numeric = apple, package name = google).",
            "schema": {
              "type": "string",
              "enum": [
                "apple",
                "google"
              ]
            },
            "example": "apple"
          },
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "Numeric App Store ID (the number after \"id\" in apps.apple.com/.../id324684580), Android package name (the id= value in play.google.com/store/apps/details?id=com.spotify.music), or the full store URL (URL-encoded).",
            "schema": {
              "type": "string",
              "maxLength": 300
            },
            "example": "324684580"
          },
          {
            "name": "country",
            "in": "query",
            "required": false,
            "description": "Two-letter store country code. App Store ratings and reviews are per country; on Google Play it sets the storefront (gl).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z]{2}$",
              "default": "us"
            },
            "example": "us"
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Google Play only: language of the page, texts and reviews (hl), e.g. en, de, pt-BR. Ignored for the App Store.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z]{2,3}([-_][A-Za-z]{2,4})?$",
              "default": "en"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The app's metadata.",
            "headers": {
              "X-Cache": {
                "description": "HIT when the store data was served from the edge cache, MISS when it was read from the store just now.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "HIT",
                    "MISS"
                  ]
                }
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/App"
                },
                "example": {
                  "store": "apple",
                  "id": "324684580",
                  "country": "us",
                  "lang": null,
                  "title": "Spotify: Music and Podcasts",
                  "developer": "Spotify",
                  "developer_id": "324684583",
                  "url": "https://apps.apple.com/us/app/spotify-music-and-podcasts/id324684580",
                  "icon": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/5f/b6/e0/5fb6e0c6-cea6-e107-8d54-8e8fcec573ce/AppIcon-0-0-1x_U007epad-0-1-0-0-sRGB-85-220.png/512x512bb.jpg",
                  "genre": "Music",
                  "price": 0,
                  "currency": "USD",
                  "free": true,
                  "rating": 4.77,
                  "ratings_count": 42443544,
                  "reviews_count": null,
                  "histogram": {
                    "1": 929581,
                    "2": 367536,
                    "3": 941478,
                    "4": 2975892,
                    "5": 37240339
                  },
                  "rating_current_version": 4.77,
                  "ratings_count_current_version": 42443544,
                  "installs": null,
                  "min_installs": null,
                  "version": "9.1.88",
                  "release_notes": "We’re always making changes and improvements to Spotify. To make sure you don’t miss a thing, just keep your Updates turned on.",
                  "released_at": "2011-07-14T11:22:37.000Z",
                  "updated_at": "2026-10-01T15:03:03.000Z",
                  "content_rating": "12+",
                  "min_os_version": "16.1",
                  "size_bytes": 313354240,
                  "contains_ads": null,
                  "in_app_purchases": null,
                  "description": "With the Spotify app, you can explore an extensive library of music and podcasts for free. Curate the best playlists and stream millions of free songs, albums,…",
                  "fetched_at": "2026-10-07T10:32:58.810Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/AppBadRequest"
          },
          "404": {
            "$ref": "#/components/responses/AppNotFound"
          },
          "429": {
            "$ref": "#/components/responses/AppRateLimited"
          },
          "502": {
            "$ref": "#/components/responses/AppUpstreamError"
          },
          "503": {
            "$ref": "#/components/responses/AppSourceBlocked"
          },
          "504": {
            "$ref": "#/components/responses/AppUpstreamTimeout"
          }
        }
      }
    },
    "/api/v1/app-reviews": {
      "get": {
        "tags": [
          "Apps"
        ],
        "operationId": "listAppReviews",
        "summary": "List App Store or Google Play reviews of an app",
        "description": "The reviews shown on the app's public store page for one country (and language on Google Play): about 10 on the App Store, about 20 on Google Play. Star filter, sorting and paging run on the cached page.",
        "parameters": [
          {
            "name": "store",
            "in": "query",
            "required": false,
            "description": "apple (App Store) or google (Google Play). Optional: inferred from id (numeric = apple, package name = google).",
            "schema": {
              "type": "string",
              "enum": [
                "apple",
                "google"
              ]
            },
            "example": "apple"
          },
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "Numeric App Store ID (the number after \"id\" in apps.apple.com/.../id324684580), Android package name (the id= value in play.google.com/store/apps/details?id=com.spotify.music), or the full store URL (URL-encoded).",
            "schema": {
              "type": "string",
              "maxLength": 300
            },
            "example": "324684580"
          },
          {
            "name": "country",
            "in": "query",
            "required": false,
            "description": "Two-letter store country code. App Store ratings and reviews are per country; on Google Play it sets the storefront (gl).",
            "schema": {
              "type": "string",
              "pattern": "^[a-z]{2}$",
              "default": "us"
            },
            "example": "us"
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Google Play only: language of the page, texts and reviews (hl), e.g. en, de, pt-BR. Ignored for the App Store.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z]{2,3}([-_][A-Za-z]{2,4})?$",
              "default": "en"
            }
          },
          {
            "name": "rating",
            "in": "query",
            "required": false,
            "description": "Only reviews with these star ratings, comma-separated, e.g. 1 or 1,2.",
            "schema": {
              "type": "string",
              "pattern": "^[1-5](,[1-5])*$"
            },
            "example": "1,2"
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "newest (default), oldest, rating_asc, rating_desc, or relevant (the order the store page shows).",
            "schema": {
              "type": "string",
              "enum": [
                "newest",
                "oldest",
                "rating_asc",
                "rating_desc",
                "relevant"
              ],
              "default": "newest"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Reviews per page.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of matching reviews to skip. Use next_offset from the previous page.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 10000,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of reviews.",
            "headers": {
              "X-Cache": {
                "description": "HIT when the store data was served from the edge cache, MISS when it was read from the store just now.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "HIT",
                    "MISS"
                  ]
                }
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppReviewsResponse"
                },
                "example": {
                  "store": "apple",
                  "id": "310633997",
                  "country": "gb",
                  "lang": null,
                  "app_title": "WhatsApp Messenger",
                  "app_url": "https://apps.apple.com/gb/app/whatsapp-messenger/id310633997",
                  "rating": 4.7,
                  "ratings_count": 4199657,
                  "fetched_at": "2026-10-07T10:33:05.390Z",
                  "total": 5,
                  "count": 2,
                  "limit": 2,
                  "offset": 0,
                  "next_offset": 2,
                  "reviews": [
                    {
                      "id": "13051642211",
                      "rating": 2,
                      "title": "Superb app but anti-disabled",
                      "text": "This is an excellent app and well designed. However the sign in process on the iPad is unnecessarily ridiculous and really hard if you’re physically disabled. If you have a physical disability like I have (tetraplegic) you have to wait to ask your support worker or someone else …",
                      "date": "2025-08-23T16:29:26.000Z",
                      "app_version": null,
                      "thumbs_up": null,
                      "developer_reply": null,
                      "developer_reply_date": null
                    },
                    {
                      "id": "12906618413",
                      "rating": 1,
                      "title": "Review from a blind persons perspective.",
                      "text": "I use WhatsApp every single day for important daily communication with my friends, family and business contacts. For four years I have enjoyed the app significantly to do things such as text messaging, voice messaging and voice calling. However, I am really disappointed with the…",
                      "date": "2025-07-18T13:17:24.000Z",
                      "app_version": null,
                      "thumbs_up": null,
                      "developer_reply": null,
                      "developer_reply_date": null
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/AppBadRequest"
          },
          "404": {
            "$ref": "#/components/responses/AppNotFound"
          },
          "429": {
            "$ref": "#/components/responses/AppRateLimited"
          },
          "502": {
            "$ref": "#/components/responses/AppUpstreamError"
          },
          "503": {
            "$ref": "#/components/responses/AppSourceBlocked"
          },
          "504": {
            "$ref": "#/components/responses/AppUpstreamTimeout"
          }
        }
      }
    },
    "/api/v1/signals": {
      "get": {
        "tags": [
          "Signals"
        ],
        "operationId": "listSignals",
        "summary": "List companies that just started hiring in a role family",
        "description": "Returns up to 20 companies per query, ranked: posts that call the role \"founding\" or \"first\" first, then signals observed on a board already tracked, then the newest. total counts every matching company; truncated is true when there are more than the 20 returned on the free tier. limit and offset page within those 20.",
        "parameters": [
          {
            "name": "family",
            "in": "query",
            "required": true,
            "description": "Role family id, or up to 5 comma-separated ids (e.g. sdr,sales,revops). GTM families: sdr, sales, revops, sales-enablement, marketing-ops, sales-engineering, partnerships, customer-success, account-management, support, professional-services, marketing, product-marketing, growth-marketing. All ids: revops, sales-enablement, marketing-ops, sales, sdr, sales-engineering, partnerships, customer-success, account-management, support, professional-services, marketing, product-marketing, growth-marketing, content, devrel, community, software-engineering, frontend, mobile, platform-sre, security, compliance, qa, engineering-management, hardware, it, data-engineering, data-analytics, ml-ai, product-management, design, ux-research, technical-writing, finance, legal, people, recruiting, bizops, workplace, executive, clinical, supply-chain.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z-]+(,[a-z-]+){0,4}$"
            },
            "example": "sdr"
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "Signals detected in the last 7, 14 or 30 days.",
            "schema": {
              "type": "string",
              "enum": [
                "7d",
                "14d",
                "30d"
              ],
              "default": "7d"
            }
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "first_hire = the role family is new on the company's careers page; surge = clearly more roles in the family in the last 30 days than in the 30 days before; all = both (one row per company).",
            "schema": {
              "type": "string",
              "enum": [
                "first_hire",
                "surge",
                "all"
              ],
              "default": "first_hire"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Companies per page.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 20,
              "default": 20
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Skip this many companies (within the top 20); use next_offset from the previous page.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 19,
              "default": 0
            }
          },
          {
            "name": "X-Client",
            "in": "header",
            "required": false,
            "description": "Optional name of your integration, used for usage stats only (never for limits). Letters, digits, / _ -, up to 64 characters, e.g. clay-http-column or clay-skill/first-team-hire-signal.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9/_-]{1,64}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching companies, best first.",
            "headers": {
              "X-Cache": {
                "description": "HIT when the result was served from the edge cache (up to 15 minutes old), MISS when it was read from the database just now.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "HIT",
                    "MISS"
                  ]
                }
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-RateLimit-Daily-Limit": {
                "description": "Requests allowed per UTC day: 50 per IP without a key, 50 per key with X-RoleDawn-Key.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Daily-Remaining": {
                "description": "Requests left today for this IP (no key) or this key.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SignalList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/SignalsBadRequest"
          },
          "401": {
            "$ref": "#/components/responses/SignalsInvalidKey"
          },
          "429": {
            "$ref": "#/components/responses/SignalsRateLimited"
          },
          "500": {
            "$ref": "#/components/responses/SignalsInternalError"
          }
        },
        "security": [
          {},
          {
            "RoleDawnKey": []
          }
        ]
      }
    }
  },
  "components": {
    "headers": {
      "RateLimitLimit": {
        "description": "Requests allowed per minute per IP.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitRemaining": {
        "description": "Requests left in the current minute.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimitReset": {
        "description": "Seconds until the window resets.",
        "schema": {
          "type": "integer"
        }
      }
    },
    "schemas": {
      "Job": {
        "type": "object",
        "required": [
          "id",
          "title",
          "company",
          "company_name",
          "ats",
          "location",
          "department",
          "remote",
          "url",
          "posted_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The job's ID in the ATS."
          },
          "title": {
            "type": "string",
            "description": "Job title as posted."
          },
          "company": {
            "type": "string",
            "description": "Board slug you requested."
          },
          "company_name": {
            "type": "string",
            "nullable": true,
            "description": "Company name when the ATS provides it (Greenhouse), otherwise null."
          },
          "ats": {
            "type": "string",
            "enum": [
              "greenhouse",
              "lever",
              "ashby"
            ]
          },
          "location": {
            "type": "string",
            "nullable": true,
            "description": "Location text; several locations are joined with \"; \"."
          },
          "department": {
            "type": "string",
            "nullable": true,
            "description": "Department, or \"Department / Team\" when the ATS has both."
          },
          "remote": {
            "type": "boolean",
            "description": "true when the ATS marks the job remote or the location says Remote."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "nullable": true,
            "description": "Public job post (apply page)."
          },
          "posted_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "First published (Greenhouse, Ashby) or created (Lever) time, ISO 8601 UTC."
          }
        }
      },
      "JobsResponse": {
        "type": "object",
        "required": [
          "ats",
          "company",
          "company_name",
          "board_url",
          "fetched_at",
          "total",
          "count",
          "limit",
          "offset",
          "next_offset",
          "jobs"
        ],
        "properties": {
          "ats": {
            "type": "string",
            "enum": [
              "greenhouse",
              "lever",
              "ashby"
            ]
          },
          "company": {
            "type": "string"
          },
          "company_name": {
            "type": "string",
            "nullable": true
          },
          "board_url": {
            "type": "string",
            "format": "uri",
            "description": "The public careers board."
          },
          "fetched_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the board was read from the ATS."
          },
          "total": {
            "type": "integer",
            "description": "Jobs matching your filters."
          },
          "count": {
            "type": "integer",
            "description": "Jobs in this page."
          },
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          },
          "next_offset": {
            "type": "integer",
            "nullable": true,
            "description": "offset for the next page, or null on the last page."
          },
          "jobs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Job"
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message",
              "docs"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "missing_parameter",
                  "invalid_parameter",
                  "company_not_found",
                  "app_not_found",
                  "not_found",
                  "method_not_allowed",
                  "rate_limited",
                  "invalid_key",
                  "upstream_error",
                  "upstream_unavailable",
                  "source_blocked",
                  "upstream_timeout",
                  "internal_error"
                ]
              },
              "message": {
                "type": "string"
              },
              "param": {
                "type": "string",
                "description": "The query parameter that caused the error, when there is one."
              },
              "docs": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "App": {
        "type": "object",
        "required": [
          "store",
          "id",
          "country",
          "title",
          "url",
          "fetched_at"
        ],
        "properties": {
          "store": {
            "type": "string",
            "enum": [
              "apple",
              "google"
            ]
          },
          "id": {
            "type": "string",
            "description": "App Store ID or Android package name"
          },
          "country": {
            "type": "string"
          },
          "lang": {
            "type": "string",
            "nullable": true,
            "description": "Google Play language used (null for the App Store)"
          },
          "title": {
            "type": "string"
          },
          "developer": {
            "type": "string",
            "nullable": true,
            "description": "Developer / seller name"
          },
          "developer_id": {
            "type": "string",
            "nullable": true,
            "description": "Store developer ID"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Public store page"
          },
          "icon": {
            "type": "string",
            "nullable": true,
            "description": "Icon URL"
          },
          "genre": {
            "type": "string",
            "nullable": true,
            "description": "Primary category"
          },
          "price": {
            "type": "number",
            "nullable": true,
            "description": "Price in currency (0 = free)"
          },
          "currency": {
            "type": "string",
            "nullable": true,
            "description": "ISO 4217 currency code"
          },
          "free": {
            "type": "boolean",
            "nullable": true,
            "description": "true when the price is 0"
          },
          "rating": {
            "type": "number",
            "nullable": true,
            "description": "Average star rating, 2 decimals"
          },
          "ratings_count": {
            "type": "integer",
            "nullable": true,
            "description": "Number of ratings"
          },
          "reviews_count": {
            "type": "integer",
            "nullable": true,
            "description": "Number of written reviews (Google Play only)"
          },
          "histogram": {
            "type": "object",
            "nullable": true,
            "description": "Number of ratings per star (1-5). App Store: from the app page of that country. Google Play: all ratings.",
            "properties": {
              "1": {
                "type": "integer"
              },
              "2": {
                "type": "integer"
              },
              "3": {
                "type": "integer"
              },
              "4": {
                "type": "integer"
              },
              "5": {
                "type": "integer"
              }
            }
          },
          "rating_current_version": {
            "type": "number",
            "nullable": true,
            "description": "Average rating of the current version (App Store only)"
          },
          "ratings_count_current_version": {
            "type": "integer",
            "nullable": true,
            "description": "Ratings of the current version (App Store only)"
          },
          "installs": {
            "type": "string",
            "nullable": true,
            "description": "Install bracket as shown, e.g. 1,000,000,000+ (Google Play only)"
          },
          "min_installs": {
            "type": "integer",
            "nullable": true,
            "description": "Lower bound of the install bracket (Google Play only)"
          },
          "version": {
            "type": "string",
            "nullable": true,
            "description": "Current version (App Store; Google Play when the page shows one)"
          },
          "release_notes": {
            "type": "string",
            "nullable": true,
            "description": "What's new in the latest release, plain text"
          },
          "released_at": {
            "type": "string",
            "nullable": true,
            "description": "First release date, ISO 8601 UTC",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "nullable": true,
            "description": "Latest update, ISO 8601 UTC",
            "format": "date-time"
          },
          "content_rating": {
            "type": "string",
            "nullable": true,
            "description": "Age rating, e.g. 12+ or Teen"
          },
          "min_os_version": {
            "type": "string",
            "nullable": true,
            "description": "Minimum iOS version (App Store only)"
          },
          "size_bytes": {
            "type": "integer",
            "nullable": true,
            "description": "Download size (App Store only)"
          },
          "contains_ads": {
            "type": "boolean",
            "nullable": true,
            "description": "Contains ads (Google Play only)"
          },
          "in_app_purchases": {
            "type": "boolean",
            "nullable": true,
            "description": "Offers in-app purchases (Google Play only)"
          },
          "description": {
            "type": "string",
            "nullable": true,
            "description": "Store description, plain text"
          },
          "fetched_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the store was read"
          }
        }
      },
      "Review": {
        "type": "object",
        "required": [
          "id",
          "rating",
          "text"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Review ID in the store"
          },
          "rating": {
            "type": "integer",
            "minimum": 1,
            "maximum": 5
          },
          "title": {
            "type": "string",
            "nullable": true,
            "description": "Review title (App Store only)"
          },
          "text": {
            "type": "string"
          },
          "date": {
            "type": "string",
            "nullable": true,
            "description": "When the review was written, ISO 8601 UTC",
            "format": "date-time"
          },
          "app_version": {
            "type": "string",
            "nullable": true,
            "description": "App version the review was written for (Google Play)"
          },
          "thumbs_up": {
            "type": "integer",
            "nullable": true,
            "description": "Helpful votes (Google Play)"
          },
          "developer_reply": {
            "type": "string",
            "nullable": true,
            "description": "The developer's public reply (Google Play)"
          },
          "developer_reply_date": {
            "type": "string",
            "nullable": true,
            "description": "When the developer replied",
            "format": "date-time"
          }
        }
      },
      "AppReviewsResponse": {
        "type": "object",
        "required": [
          "store",
          "id",
          "country",
          "app_title",
          "fetched_at",
          "total",
          "count",
          "limit",
          "offset",
          "next_offset",
          "reviews"
        ],
        "properties": {
          "store": {
            "type": "string",
            "enum": [
              "apple",
              "google"
            ]
          },
          "id": {
            "type": "string"
          },
          "country": {
            "type": "string"
          },
          "lang": {
            "type": "string",
            "nullable": true
          },
          "app_title": {
            "type": "string"
          },
          "app_url": {
            "type": "string",
            "format": "uri"
          },
          "rating": {
            "type": "number",
            "nullable": true
          },
          "ratings_count": {
            "type": "integer",
            "nullable": true
          },
          "fetched_at": {
            "type": "string",
            "format": "date-time"
          },
          "total": {
            "type": "integer",
            "description": "Reviews matching the filters"
          },
          "count": {
            "type": "integer",
            "description": "Reviews in this page"
          },
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          },
          "next_offset": {
            "type": "integer",
            "nullable": true,
            "description": "offset of the next page, null on the last page"
          },
          "reviews": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Review"
            }
          }
        }
      },
      "SignalList": {
        "type": "object",
        "required": [
          "family",
          "since",
          "type",
          "window_start",
          "generated_at",
          "total",
          "count",
          "limit",
          "offset",
          "next_offset",
          "free_rows",
          "truncated",
          "signals"
        ],
        "properties": {
          "family": {
            "type": "string",
            "description": "The role families you asked for, comma-separated"
          },
          "since": {
            "type": "string",
            "enum": [
              "7d",
              "14d",
              "30d"
            ]
          },
          "type": {
            "type": "string",
            "enum": [
              "first_hire",
              "surge",
              "all"
            ]
          },
          "window_start": {
            "type": "string",
            "format": "date-time",
            "description": "Signals detected on or after this time are included"
          },
          "generated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the result was read from the database"
          },
          "total": {
            "type": "integer",
            "description": "Companies matching the query"
          },
          "count": {
            "type": "integer",
            "description": "Companies in this response"
          },
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          },
          "next_offset": {
            "type": "integer",
            "nullable": true,
            "description": "offset for the next page within the free rows, null on the last page"
          },
          "free_rows": {
            "type": "integer",
            "description": "Companies available per query on the free tier (20)"
          },
          "truncated": {
            "type": "boolean",
            "description": "true when total is larger than free_rows"
          },
          "signals": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Signal"
            }
          }
        }
      },
      "Signal": {
        "type": "object",
        "required": [
          "company",
          "company_key",
          "company_url",
          "title",
          "family",
          "type",
          "signal",
          "evidence",
          "founding",
          "job_url",
          "detected"
        ],
        "properties": {
          "company": {
            "type": "string",
            "description": "Company name"
          },
          "company_key": {
            "type": "string",
            "description": "RoleDawn company id (ats-slug)"
          },
          "company_url": {
            "type": "string",
            "format": "uri",
            "description": "RoleDawn company page with all open roles"
          },
          "title": {
            "type": "string",
            "description": "Title of the job post behind the signal"
          },
          "family": {
            "type": "string",
            "description": "Role family id"
          },
          "type": {
            "type": "string",
            "enum": [
              "first_hire",
              "surge"
            ]
          },
          "signal": {
            "type": "string",
            "description": "Readable signal, e.g. \"New SDR/BDR team\" or \"RevOps hiring surge\""
          },
          "evidence": {
            "type": "string",
            "description": "\"New on their board\" (observed on a board already tracked), \"Likely new team · inferred\" (estimate for a recently tracked board), or for surges the role counts, e.g. \"5 roles in 30 days vs 1 before\""
          },
          "founding": {
            "type": "boolean",
            "description": "The job post calls the role \"founding\" or \"first\""
          },
          "location": {
            "type": "string",
            "nullable": true,
            "description": "Location text of the job post"
          },
          "job_url": {
            "type": "string",
            "format": "uri",
            "description": "The company's own public job post"
          },
          "posted": {
            "type": "string",
            "nullable": true,
            "format": "date-time",
            "description": "When the job was posted (or first seen), ISO 8601 UTC"
          },
          "detected": {
            "type": "string",
            "format": "date-time",
            "description": "When RoleDawn detected the signal, ISO 8601 UTC"
          },
          "roles_last_30d": {
            "type": "integer",
            "nullable": true,
            "description": "Surges only: roles in the family posted in the last 30 days"
          },
          "roles_prior_30d": {
            "type": "integer",
            "nullable": true,
            "description": "Surges only: roles in the family posted in the 30 days before"
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Missing or invalid parameter (codes missing_parameter, invalid_parameter).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "invalid_parameter",
                "message": "ats must be one of greenhouse, lever, ashby.",
                "param": "ats",
                "docs": "https://roledawn.com/api#errors"
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "No public job board with this slug on this ATS (code company_not_found).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "company_not_found",
                "message": "No public greenhouse job board found for \"acme-xyz\". Check the slug in the company's careers-page URL.",
                "param": "company",
                "docs": "https://roledawn.com/api#errors"
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "Fair-use limit reached (code rate_limited). Wait Retry-After seconds.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "UpstreamError": {
        "description": "The ATS returned an error (code upstream_error). Retry later.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "SourceBlocked": {
        "description": "The job board cannot be read right now: the ATS is temporarily unavailable or throttling requests (code upstream_unavailable, with a Retry-After header), or the ATS host's robots.txt does not allow the request or could not be loaded (code source_blocked). Retry later.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "UpstreamTimeout": {
        "description": "The ATS did not answer within 30 seconds (code upstream_timeout).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "AppBadRequest": {
        "description": "Missing or invalid parameter (codes missing_parameter, invalid_parameter).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "invalid_parameter",
                "message": "id must be a numeric App Store ID, an Android package name or an App Store / Google Play URL.",
                "param": "id",
                "docs": "https://roledawn.com/api/app-reviews#errors"
              }
            }
          }
        }
      },
      "AppNotFound": {
        "description": "No app with this ID in this store and country (code app_not_found).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "app_not_found",
                "message": "No App Store app with ID 999999999999 in the \"us\" store. Check the number after \"id\" in the apps.apple.com URL, or try another country.",
                "param": "id",
                "docs": "https://roledawn.com/api/app-reviews#errors"
              }
            }
          }
        }
      },
      "AppRateLimited": {
        "description": "Fair-use limit reached (code rate_limited). Wait Retry-After seconds.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "AppUpstreamError": {
        "description": "The store returned an error (code upstream_error). Retry later.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "AppSourceBlocked": {
        "description": "The store cannot be read right now: it is temporarily unavailable or throttling requests (code upstream_unavailable, with a Retry-After header), or its robots.txt does not allow the request (code source_blocked). Retry later.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "AppUpstreamTimeout": {
        "description": "The store did not answer within 30 seconds (code upstream_timeout). Retry later.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "SignalsBadRequest": {
        "description": "Missing or invalid parameter (codes missing_parameter, invalid_parameter).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "invalid_parameter",
                "message": "since must be one of 7d, 14d, 30d.",
                "param": "since",
                "docs": "https://roledawn.com/api#errors"
              }
            }
          }
        }
      },
      "SignalsInvalidKey": {
        "description": "Unknown X-RoleDawn-Key (code invalid_key). Check the key, drop the header to use the per-IP tier, or get a new free key at https://roledawn.com/api/key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "invalid_key",
                "message": "Unknown X-RoleDawn-Key. Check the key, remove the header to use the per-IP free tier, or get a new free key: https://roledawn.com/api/key",
                "docs": "https://roledawn.com/api#errors"
              }
            }
          }
        }
      },
      "SignalsRateLimited": {
        "description": "Limit reached (code rate_limited): 60 requests per minute, or the daily allowance (50 per IP per UTC day without a key, 50 per key with X-RoleDawn-Key). Wait Retry-After seconds; without a key, the message links the free key page and the Weekly List trial.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "rate_limited",
                "message": "This IP has used its 50 free Hiring Signals requests today. Shared IPs (Clay, hosted AI agents) reach this sooner: get a free personal API key at https://roledawn.com/api/key?utm_source=api-429 and send it in the X-RoleDawn-Key header. The full weekly list with suggested openers has a free trial: https://roledawn.com/weekly-list?utm_source=api-429",
                "docs": "https://roledawn.com/api#errors"
              }
            }
          }
        }
      },
      "SignalsInternalError": {
        "description": "Unexpected error (code internal_error). Retry later.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "securitySchemes": {
      "RoleDawnKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-RoleDawn-Key",
        "description": "Optional free personal key from https://roledawn.com/api/key (email only). Moves the daily count from your IP to your key: 50 requests per key per UTC day. Applies to the Hiring Signals API only."
      }
    }
  }
}