openapi: "3.1.0"
info:
  title: jobapi.in
  version: "1"
  description: AI-ready job board API for IT developers.
  contact:
    url: https://jobapi.in

servers:
  - url: https://jobapi.in/v1

tags:
  - name: jobs
    description: Job listing management and retrieval.
  - name: technologies
    description: Available technology tags for filtering job listings.
  - name: checkout
    description: Stripe checkout session creation for purchasing API credits.
  - name: webhooks
    description: Stripe webhook event handling.

components:
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      description: API key obtained after purchasing a credit package.

  headers:
    X-RateLimit-Limit:
      description: Maximum number of requests allowed per window.
      schema:
        type: integer
        example: 100
    X-RateLimit-Remaining:
      description: Number of requests remaining in the current window.
      schema:
        type: integer
        example: 95
    X-RateLimit-Reset:
      description: Unix timestamp when the rate limit window resets.
      schema:
        type: integer
        example: 1748476800

  parameters:
    cursor:
      name: cursor
      in: query
      required: false
      description: Opaque cursor for pagination.
      example: "01HQ9Z8XKJFQ3VMGJDWG8QSFH"
      schema:
        type: string
    limit:
      name: limit
      in: query
      required: false
      description: Maximum number of results to return.
      example: 20
      schema:
        type: integer
        default: 20
        maximum: 100
    s:
      name: s
      in: query
      required: false
      description: Full-text search across job title and requirements.
      example: "golang backend"
      schema:
        type: string
    req:
      name: req
      in: query
      required: false
      description: Comma-separated technology filter.
      example: "kubernetes,aws"
      schema:
        type: string
    jobId:
      name: jobId
      in: path
      required: true
      description: ULID identifier of the job.
      example: "01HQ9Z8XKJFQ3VMGJDWG8QSFH"
      schema:
        type: string

  schemas:
    Company:
      type: object
      description: Employer company details attached to a job listing.
      required:
        - name
      properties:
        name:
          type: string
          description: Company name.
          example: "Acme Corp"
        url:
          type: ["string", "null"]
          description: Company website URL. Null when not provided.
          example: "https://acme.example.com"

    Salary:
      type: object
      description: Salary range with currency and payment period.
      required:
        - min
        - max
        - currency
        - period
      properties:
        min:
          type: integer
          description: Minimum salary amount.
          example: 10000
        max:
          type: integer
          description: Maximum salary amount.
          example: 18000
        currency:
          type: string
          enum: [PLN, EUR, USD, GBP]
          description: ISO 4217 currency code.
          example: "PLN"
        period:
          type: string
          enum: [monthly, yearly]
          description: Salary payment period.
          example: "monthly"

    Job:
      type: object
      description: A job listing on jobapi.in.
      required:
        - id
        - company
        - title
        - description
        - requirements
        - responsibilities
        - salary
        - workMode
        - country
        - city
        - status
        - expiresAt
        - expiredAt
        - createdAt
      properties:
        id:
          type: string
          description: ULID unique identifier for the job listing.
          example: "01HQ9Z8XKJFQ3VMGJDWG8QSFH"
        company:
          $ref: '#/components/schemas/Company'
        title:
          type: string
          description: Job title.
          example: "Senior Go Engineer"
        description:
          type: string
          maxLength: 100
          description: Short job description, maximum 100 characters.
          example: "Build the next generation of cloud-native APIs."
        requirements:
          type: array
          description: Required technologies from the predefined list.
          items:
            type: string
          example: ["Go", "Kubernetes", "PostgreSQL"]
        responsibilities:
          type: array
          description: List of job responsibilities.
          items:
            type: string
          example: ["Design and implement REST APIs", "Maintain PostgreSQL schemas"]
        salary:
          $ref: '#/components/schemas/Salary'
        workMode:
          type: string
          enum: [remote, hybrid, onsite]
          description: Work arrangement for this position.
          example: "remote"
        country:
          type: ["string", "null"]
          description: Country where the job is located. Null for remote roles.
          example: "Poland"
        city:
          type: ["string", "null"]
          description: City where the job is located. Null for remote roles.
          example: "Warsaw"
        status:
          type: string
          enum: [active, expired]
          description: Current status of the job listing.
          example: "active"
        expiresAt:
          type: string
          format: date
          description: Calendar date when the listing expires, in YYYY-MM-DD format.
          example: "2026-06-28"
        expiredAt:
          type: ["string", "null"]
          format: date-time
          description: ISO 8601 UTC timestamp when the listing expired. Null while the listing is active.
          example: "2026-05-01T00:00:00Z"
        createdAt:
          type: string
          format: date-time
          description: ISO 8601 UTC timestamp when the listing was created.
          example: "2026-05-29T10:00:00Z"

    CreateJobRequest:
      type: object
      description: Request body for creating a new job listing.
      required:
        - title
        - description
        - requirements
        - responsibilities
        - salary
        - workMode
      properties:
        title:
          type: string
          description: Job title.
          example: "Senior Go Engineer"
        description:
          type: string
          maxLength: 100
          description: Short job description, maximum 100 characters.
          example: "Build the next generation of cloud-native APIs."
        requirements:
          type: array
          description: Required technologies from the predefined list.
          items:
            type: string
          example: ["Go", "Kubernetes", "PostgreSQL"]
        responsibilities:
          type: array
          description: List of job responsibilities.
          items:
            type: string
          example: ["Design and implement REST APIs", "Maintain PostgreSQL schemas"]
        salary:
          $ref: '#/components/schemas/Salary'
        workMode:
          type: string
          enum: [remote, hybrid, onsite]
          description: Work arrangement for this position.
          example: "remote"
        country:
          type: string
          description: Country where the job is located. Null for remote roles.
          example: "Poland"
        city:
          type: string
          description: City where the job is located. Null for remote roles.
          example: "Warsaw"

    Pagination:
      type: object
      description: Cursor-based pagination metadata.
      required:
        - nextCursor
        - hasMore
      properties:
        nextCursor:
          type: ["string", "null"]
          description: Opaque cursor pointing to the next page. Null if there are no more results.
          example: "01HQ9Z8XKJFQ3VMGJDWG8QSFJ"
        hasMore:
          type: boolean
          description: Indicates whether more results exist after this page.
          example: true

    Meta:
      type: object
      description: Response metadata.
      required:
        - total
      properties:
        total:
          type: integer
          description: Total count of items matching the query.
          example: 42

    JobsListResponse:
      type: object
      description: Envelope response for a paginated list of job listings.
      required:
        - data
        - pagination
        - meta
      properties:
        data:
          type: array
          description: List of job listings for the current page.
          items:
            $ref: '#/components/schemas/Job'
          example: []
        pagination:
          $ref: '#/components/schemas/Pagination'
        meta:
          $ref: '#/components/schemas/Meta'

    TechnologiesListResponse:
      type: object
      description: Envelope response for the complete list of available technologies. Pagination is always null as the full list is returned in one response.
      required:
        - data
        - pagination
        - meta
      properties:
        data:
          type: array
          description: Complete list of available technology names.
          items:
            type: string
          example: ["Go", "Kubernetes", "PostgreSQL"]
        pagination:
          type: "null"
          description: Always null — the full technology list is returned in one response.
        meta:
          $ref: '#/components/schemas/Meta'

    CheckoutRequest:
      type: object
      description: Request body for creating a Stripe checkout session to purchase API credits.
      required:
        - package
        - companyName
        - email
      properties:
        package:
          type: string
          enum: [starter, standard, pro]
          description: Credit package to purchase.
          example: "standard"
        companyName:
          type: string
          description: Company name, stored permanently on the created API key.
          example: "Acme Corp"
        companyUrl:
          type: string
          description: Company website URL. Must be a valid URL whose domain resolves via DNS.
          example: "https://acme.example.com"
        email:
          type: string
          format: email
          description: Email address where the API key is delivered after successful payment.
          example: "hiring@acme.example.com"

    CheckoutResponse:
      type: object
      description: Stripe-hosted checkout session URL to redirect the user to.
      required:
        - checkoutUrl
      properties:
        checkoutUrl:
          type: string
          description: Stripe-hosted checkout URL.
          example: "https://checkout.stripe.com/pay/cs_test_a1b2c3d4e5f6"

    WebhookResponse:
      type: object
      description: Acknowledgement response confirming receipt of a Stripe webhook event.
      required:
        - received
      properties:
        received:
          type: boolean
          description: Confirms the webhook event was received and will be processed.
          example: true

    Problem:
      type: object
      description: RFC 9457 Problem Details object used for all error responses.
      required:
        - type
        - title
        - status
      properties:
        type:
          type: string
          description: A URI reference that identifies the problem type.
          example: "https://jobapi.in/problems/validation-error"
        title:
          type: string
          description: A short, human-readable summary of the problem type.
          example: "Validation Error"
        status:
          type: integer
          description: The HTTP status code for this occurrence of the problem.
          example: 400
        detail:
          type: string
          description: A human-readable explanation specific to this occurrence of the problem.
          example: "salary.min must be a positive integer"

paths:
  /jobs:
    get:
      operationId: listJobs
      tags: [jobs]
      summary: List job listings
      description: Returns a paginated list of active job listings. Supports full-text search and technology filtering.
      x-cors:
        allowOrigins: "*"
      parameters:
        - $ref: '#/components/parameters/cursor'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/s'
        - $ref: '#/components/parameters/req'
      responses:
        "200":
          description: Paginated list of job listings.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobsListResponse'
        "429":
          description: Rate limit exceeded.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'

    post:
      operationId: createJob
      tags: [jobs]
      summary: Create a job listing
      description: Creates a new job listing. Costs one credit from the authenticated API key on success. Schema validation failures never cost credits.
      security:
        - ApiKeyAuth: []
      x-cors:
        allowOrigins: "https://jobapi.in"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateJobRequest'
      responses:
        "201":
          description: Job listing created successfully.
          headers:
            Location:
              description: URL of the newly created job listing.
              schema:
                type: string
                example: "/jobs/01HQ9Z8XKJFQ3VMGJDWG8QSFH"
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Job'
        "400":
          description: Request body failed schema validation. No credit is charged.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        "401":
          description: Missing or invalid API key.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        "402":
          description: Insufficient credits on the API key.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        "422":
          description: Request passed schema validation but failed business logic rules (e.g. unrecognised technology tag).
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        "429":
          description: Rate limit exceeded.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'

  /jobs/{jobId}:
    get:
      operationId: getJob
      tags: [jobs]
      summary: Get a job listing
      description: Returns a single job listing by its ULID identifier.
      x-cors:
        allowOrigins: "*"
      parameters:
        - $ref: '#/components/parameters/jobId'
      responses:
        "200":
          description: The requested job listing.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Job'
        "404":
          description: Job listing not found.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        "429":
          description: Rate limit exceeded.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'

  /technologies:
    get:
      operationId: listTechnologies
      tags: [technologies]
      summary: List available technologies
      description: Returns the complete list of technology tags available for filtering and tagging job listings.
      x-cors:
        allowOrigins: "*"
      responses:
        "200":
          description: Complete list of available technologies.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TechnologiesListResponse'
        "429":
          description: Rate limit exceeded.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'

  /checkout:
    post:
      operationId: createCheckout
      tags: [checkout]
      summary: Create a checkout session
      description: Creates a Stripe checkout session for purchasing an API credit package. Returns a redirect URL to the Stripe-hosted payment page.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CheckoutRequest'
      responses:
        "200":
          description: Checkout session created successfully.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CheckoutResponse'
        "400":
          description: Request body failed schema validation.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        "422":
          description: Request passed schema validation but failed business logic rules (e.g. unresolvable domain in companyUrl).
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        "429":
          description: Rate limit exceeded.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'

  /webhooks/stripe:
    post:
      operationId: stripeWebhook
      tags: [webhooks]
      summary: Handle Stripe webhook
      description: Receives and processes Stripe webhook events. Authenticated via the Stripe-Signature header verified against the webhook signing secret.
      requestBody:
        required: true
        description: Raw Stripe event payload as delivered by Stripe.
        content:
          application/json:
            schema:
              type: object
              description: Raw Stripe webhook event payload.
              example:
                id: "evt_1OxdBxLkN5DKcBGmAbCdEfGh"
                type: "checkout.session.completed"
      responses:
        "200":
          description: Webhook event received and accepted for processing.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookResponse'
        "400":
          description: Invalid Stripe signature or malformed payload.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/X-RateLimit-Limit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/X-RateLimit-Remaining'
            X-RateLimit-Reset:
              $ref: '#/components/headers/X-RateLimit-Reset'
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
