{
  "openapi": "3.1.0",
  "info": {
    "title": "JobsPipe API",
    "version": "1.0.0",
    "description": "JobsPipe is a unified data API over public job and technographic sources. This document covers the live public endpoints: a job-postings search that returns normalized records matching a rich filter set, and an on-demand technology-stack scanner that detects the tools a given domain runs. Authenticate with an API key issued from the JobsPipe dashboard, sent as an HTTP bearer token.\n\nReliability for agents: write requests accept an Idempotency-Key header so retries are safe; replays return the original response with Idempotent-Replayed: true. Responses carry RFC RateLimit headers (RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset, RateLimit-Policy) and a Retry-After header on 429.\n\nVersioning and deprecation policy: the API is versioned in the URL path (/v1). Breaking changes ship under a new version path; an endpoint is only deprecated with at least 6 months notice, signalled with Deprecation and Sunset response headers and announced in the changelog at https://jobspipe.dev/changelog.\n\nSandbox / test environment: a public sandbox lives under the base path /v1/sandbox (every route below it, no key required) and consumes no quota. It returns canned sample data shaped exactly like the live endpoints so agents can exercise the API safely, including a batch/bulk search and an asynchronous export that follows a 202 Accepted plus poll pattern: POST /v1/sandbox/jobs/export returns 202 with a Location header pointing at a status URL you poll with GET until the job is complete.\n\nError handling and retries: every error is a JSON object with a machine-readable error code and optional message, never an HTML page. Retry a 429 after the Retry-After header using exponential backoff; 502 and 504 are transient and safe to retry with backoff; pair any retry with an Idempotency-Key so it cannot duplicate work. Live service health is exposed at https://api.jobspipe.dev/healthz and https://api.jobspipe.dev/readiness.",
    "contact": {
      "name": "JobsPipe",
      "url": "https://jobspipe.dev",
      "email": "support@jobspipe.dev"
    }
  },
  "x-service-info": {
    "categories": [
      "data",
      "jobs",
      "technographics"
    ],
    "docs": {
      "homepage": "https://jobspipe.dev",
      "apiReference": "https://jobspipe.dev/docs",
      "llms": "https://jobspipe.dev/llms.txt"
    }
  },
  "servers": [
    {
      "url": "https://api.jobspipe.dev"
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "webhooks": {
    "job.created": {
      "post": {
        "summary": "New job posting matched a subscription",
        "description": "JobsPipe POSTs this event to your configured webhook URL when a new posting matches a saved subscription. Verify authenticity with the X-JobsPipe-Signature header: a hex-encoded HMAC-SHA256 of the raw request body computed with your webhook signing secret. Compare it in constant time and reject on mismatch. The X-JobsPipe-Timestamp header (Unix seconds) guards against replay - reject deliveries older than five minutes. Signing secrets are issued and rotated from the dashboard.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Job"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Acknowledged. Respond 2xx within 5s or JobsPipe retries with exponential backoff."
          }
        }
      }
    }
  },
  "paths": {
    "/v1/jobs/search": {
      "post": {
        "operationId": "searchJobs",
        "summary": "Search job postings",
        "description": "Returns live job postings matching the supplied filters, normalized into a single schema. The body is a JSON object of optional filters; all filters combine with AND, and array filters ending in _or match any of their values. An empty body returns the most recent postings up to your plan's page size.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JobSearchRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A page of matching job postings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobSearchResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "Monthly request quota exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Per-second rate limit exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/stack/scan": {
      "post": {
        "operationId": "scanStack",
        "summary": "Scan a domain's technology stack",
        "description": "Scans a single domain on demand and returns the technologies detected on it. Results are cached. Requires authentication with an API key.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StackScanRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The scan result for the requested domain.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StackScanResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request body was missing a domain, contained an invalid domain, or could not be parsed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "Monthly request quota exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Per-second rate limit exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitErrorResponse"
                }
              }
            }
          },
          "502": {
            "description": "The scanner failed to complete the scan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "504": {
            "description": "The scan timed out.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/sandbox/jobs/search": {
      "post": {
        "operationId": "sandboxSearchJobs",
        "summary": "Sandbox job search (free test environment, sample data)",
        "description": "Free sandbox / test environment. No API key required and no quota is consumed. Accepts any or empty JSON body and returns canned sample job postings shaped exactly like the live /v1/jobs/search response so agents can exercise the API safely.",
        "security": [],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JobSearchRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sample job search results.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobSearchResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/sandbox/jobs/search/batch": {
      "post": {
        "operationId": "sandboxSearchJobsBatch",
        "summary": "Sandbox batch / bulk job search (sample data)",
        "description": "Free sandbox / test environment, no API key required and no quota consumed. Accepts a requests array (max 10) of job-search filter objects and returns one entry per request, each echoing the sample job-search payload. Returns 400 if requests is missing or not an array.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "requests"
                ],
                "properties": {
                  "requests": {
                    "type": "array",
                    "maxItems": 10,
                    "items": {
                      "$ref": "#/components/schemas/JobSearchRequest"
                    },
                    "description": "Up to 10 job-search filter objects to run in a single call."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One response entry per request, in order.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "responses"
                  ],
                  "properties": {
                    "responses": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "status",
                          "body"
                        ],
                        "properties": {
                          "status": {
                            "type": "integer"
                          },
                          "body": {
                            "$ref": "#/components/schemas/JobSearchResponse"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The requests field was missing or not an array.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/sandbox/jobs/export": {
      "post": {
        "operationId": "sandboxCreateExport",
        "summary": "Create a sandbox export job (async 202 + poll pattern)",
        "description": "Free sandbox / test environment, no API key required. Demonstrates the asynchronous job pattern: returns 202 Accepted with a Location header pointing at the job's status URL. Poll GET /v1/sandbox/jobs/export/{id} until the job is completed, then fetch the result from its result_url.",
        "security": [],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JobSearchRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Export job accepted. Poll the status URL provided in the Location header and in the body.",
            "headers": {
              "Location": {
                "description": "Absolute URL to poll for this export job's status.",
                "schema": {
                  "type": "string",
                  "format": "uri"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "job_id",
                    "status",
                    "status_url"
                  ],
                  "properties": {
                    "job_id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "completed"
                      ]
                    },
                    "status_url": {
                      "type": "string",
                      "format": "uri",
                      "description": "URL to poll for this job's status."
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/sandbox/jobs/export/{id}": {
      "get": {
        "operationId": "sandboxGetExport",
        "summary": "Poll a sandbox export job's status",
        "description": "Free sandbox / test environment, no API key required. Returns the status of a previously created export job plus a result_url from which the sample data can be fetched.",
        "security": [],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The export job id returned by POST /v1/sandbox/jobs/export."
          }
        ],
        "responses": {
          "200": {
            "description": "The export job's current status.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "job_id",
                    "status",
                    "result_url"
                  ],
                  "properties": {
                    "job_id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "completed"
                      ]
                    },
                    "result_url": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No export job exists for that id, or it expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key issued from the JobsPipe dashboard, prefixed jp_live_, sent as a bearer token in the Authorization header."
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string",
          "maxLength": 255
        },
        "description": "Optional client-generated key (e.g. a UUID) that makes this POST safe to retry. A repeat with the same key returns the original response plus an Idempotent-Replayed: true header. Keys are retained for 24 hours."
      }
    },
    "schemas": {
      "JobSearchRequest": {
        "type": "object",
        "description": "Optional filters for the job search. All fields are optional and combine with AND. Array filters ending in _or match any of their values; those ending in _not exclude their values.",
        "properties": {
          "job_title_or": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Match jobs whose title contains any of these."
          },
          "job_title_not": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Exclude jobs whose title contains any of these."
          },
          "description_or": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Match jobs whose description contains any of these phrases."
          },
          "description_not": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Exclude jobs whose description contains any of these phrases."
          },
          "job_country_code_or": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Match any of these ISO alpha-2 country codes, e.g. US, GB."
          },
          "job_country_code_not": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Exclude these ISO alpha-2 country codes."
          },
          "job_seniority_or": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Match any of these seniority levels."
          },
          "employment_type_or": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "full-time",
                "part-time",
                "contract",
                "temporary",
                "internship"
              ]
            },
            "description": "Match any of these employment types."
          },
          "source_or": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Match jobs from any of these sources."
          },
          "company_name_partial_match_or": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Match any of these company names (partial / contains)."
          },
          "remote": {
            "type": "boolean",
            "description": "true for remote-only, false to exclude remote."
          },
          "posted_at_max_age_days": {
            "type": "integer",
            "description": "Only postings newer than this many days."
          },
          "posted_at_gte": {
            "type": "string",
            "description": "Only postings on or after this date (YYYY-MM-DD)."
          },
          "posted_at_lte": {
            "type": "string",
            "description": "Only postings on or before this date (YYYY-MM-DD)."
          },
          "limit": {
            "type": "integer",
            "description": "Maximum number of results to return, capped by your plan's page size."
          },
          "include_total_results": {
            "type": "boolean",
            "description": "Include the total match count in metadata.total_results."
          }
        }
      },
      "Job": {
        "type": "object",
        "description": "A normalized job posting.",
        "properties": {
          "id": {
            "type": "string"
          },
          "job_title": {
            "type": "string"
          },
          "company": {
            "type": "string"
          },
          "location": {
            "type": [
              "string",
              "null"
            ]
          },
          "country_code": {
            "type": [
              "string",
              "null"
            ]
          },
          "remote": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "seniority": {
            "type": [
              "string",
              "null"
            ]
          },
          "date_posted": {
            "type": [
              "string",
              "null"
            ]
          },
          "final_url": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "JobSearchResponse": {
        "type": "object",
        "required": [
          "metadata",
          "data"
        ],
        "properties": {
          "metadata": {
            "type": "object",
            "description": "Counts and the pagination cursor.",
            "properties": {
              "total_results": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "truncated_results": {
                "type": "integer"
              },
              "next_cursor": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Job"
            },
            "description": "The page of matching job postings."
          }
        }
      },
      "StackScanRequest": {
        "type": "object",
        "required": [
          "domain"
        ],
        "properties": {
          "domain": {
            "type": "string",
            "description": "The domain to scan, e.g. stripe.com."
          },
          "mode": {
            "type": "string",
            "enum": [
              "auto",
              "html",
              "render"
            ],
            "description": "Optional scan mode. Defaults to auto."
          }
        }
      },
      "DetectedTech": {
        "type": "object",
        "description": "A technology detected on the scanned domain."
      },
      "StackScanResponse": {
        "type": "object",
        "required": [
          "domain",
          "scanned_at",
          "http_status",
          "render_path",
          "detected"
        ],
        "properties": {
          "domain": {
            "type": "string",
            "description": "The normalized domain that was scanned."
          },
          "scanned_at": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 timestamp of when the scan was performed."
          },
          "http_status": {
            "type": "integer",
            "description": "HTTP status code returned by the scanned domain."
          },
          "render_path": {
            "type": "string",
            "description": "The render path used to fetch the domain."
          },
          "detected": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DetectedTech"
            },
            "description": "The technologies detected on the domain."
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Machine-readable error code, such as missing_domain, invalid_domain, bad_request, unauthorized, quota_exceeded, or scanner_failed."
          },
          "message": {
            "type": "string",
            "description": "Optional human-readable detail."
          }
        }
      },
      "RateLimitErrorResponse": {
        "type": "object",
        "required": [
          "error",
          "limit",
          "count"
        ],
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "rate_limited"
            ]
          },
          "limit": {
            "type": "integer",
            "description": "The rate limit ceiling for the bucket."
          },
          "count": {
            "type": "integer",
            "description": "The caller's current count within the window."
          }
        }
      }
    }
  }
}