{
  "openapi": "3.0.3",
  "info": {
    "title": "RoleDawn Hiring Signals API",
    "version": "1.0.0",
    "description": "One 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": "How to pull hiring signals into Clay with an HTTP API column (real requests and responses)",
    "url": "https://roledawn.com/guides/clay-http-api-hiring-signals"
  },
  "servers": [
    {
      "url": "https://roledawn.com",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Signals",
      "description": "Companies that just started hiring a new team"
    }
  ],
  "paths": {
    "/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": {
    "schemas": {
      "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"
          }
        }
      },
      "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"
              }
            }
          }
        }
      }
    },
    "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"
        }
      }
    },
    "responses": {
      "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."
      }
    }
  }
}