openapi: 3.0.3
info:
  title: eCore Service API
  version: "1.0.0"
  description: |
    Programmatic access to eCore's B2B contact & company data platform:
    **DataCore search & unlock**, **email/phone enrichment**, **bulk email
    validation**, **account/contact enrichment**, a one-flow **pipeline**, and
    **webhooks**.

    ## Base URLs

    | Environment | URL |
    | --- | --- |
    | **Production** | `https://backend.ecoreservice.com/api/v1` |
    | **Test** | `https://test.ecoreservice.com/backend/api/v1` |

    The test host includes a `/backend` segment; production does not.

    ## Authentication

    Pass your API token in the `Authorization` header. Both
    `Authorization: Token <token>` and `Authorization: Bearer <token>` are
    accepted on **every** endpoint — the same token value works either way.

    ### Get your API key

    1. Sign in to the eCore dashboard at [app.ecoreservice.com](https://app.ecoreservice.com/).
    2. Open **Settings → API Tokens**.
    3. Create a token and copy it.
    4. Paste it into the **Auth** field of any endpoint's **Try It** panel below.

    ## Response envelope

    Every response is wrapped as `{ "success": true | false, "response": <payload> }`.
    The response schemas below describe the contents of `response`.

    ## Credits

    Billable operations draw credits from your wallet — email unlock **1 credit**,
    phone unlock **5 credits**, per-row enrichment from **2 credits**, and
    **Search is free**. Charge-on-success and upfront-with-refund models apply
    per endpoint.

    ## Using this playground

    1. Choose an endpoint from the sidebar on the left.
    2. Open the **Try It** panel and paste your API token into the auth field.
    3. Start with **Search Records** — it's free and confirms your token works.

    > Live requests run against your real account and spend credits (except free
    > operations like Search). CORS must allow the docs origin for in-browser
    > requests to succeed.
  contact:
    name: eCore Service
    url: https://ecoreservice.com/about-us/
servers:
  - url: https://backend.ecoreservice.com/api/v1
    description: Production
  - url: https://test.ecoreservice.com/backend/api/v1
    description: Staging

tags:
  - name: DataCore Search
  - name: Pipeline & Webhooks
  - name: Email & Phone Enrichment
  - name: Email Validation
  - name: Account & Contact Enrichment
  - name: Career Monitor

components:
  securitySchemes:
    tokenAuth:
      type: apiKey
      in: header
      name: Authorization
      description: >
        `Token <your_api_token>` (note the literal `Token ` prefix). Accepted on
        every endpoint. `Bearer <your_api_token>` is equivalent.
    bearerAuth:
      type: http
      scheme: bearer
      description: >
        `Bearer <your_api_token>`. Accepted on every endpoint and equivalent to
        the `Token` prefix.
  responses:
    Unauthorized:
      description: Missing or invalid token
      content:
        application/json:
          schema:
            type: object
            properties:
              detail:
                type: string
                example: Invalid token.
    PaymentRequired:
      description: Insufficient wallet balance; no credits deducted
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
              required:
                type: integer
              balance:
                type: integer
  schemas:
    CareerMonitorJob:
      type: object
      properties:
        job_id: { type: integer }
        title: { type: string }
        status: { type: string, enum: [Pending, Processing, Completed, Failed] }
        total_records: { type: integer }
        processed_records: { type: integer }
        submitted_at: { type: string, format: date-time }
        started_at: { type: string, format: date-time, nullable: true }
        completed_at: { type: string, format: date-time, nullable: true }
        total_credits_charged: { type: number }
        credit_charge_failed: { type: boolean }
        credit_charge_failure_reason: { type: string, nullable: true }
    CareerMonitorRecord:
      type: object
      properties:
        ec_id: { type: string }
        first_name: { type: string }
        last_name: { type: string }
        current_title: { type: string }
        contact_location: { type: string }
        original_company_name: { type: string }
        matched_company_name: { type: string }
        company_website: { type: string }
        status:
          type: string
          enum:
            - Still in the company
            - Left the company
            - Uncertain
            - Unable to match
            - Broken LI
            - Error
            - Pending
            - Processing
    SearchRecord:
      type: object
      properties:
        uuid: { type: string }
        first_name: { type: string }
        last_name: { type: string }
        company_name: { type: string }
        job_title: { type: string }
        job_level: { type: string }
        industry: { type: string }
        contact_linkedin_url: { type: string }
        contact_email: { type: string, nullable: true }
        contact_direct_phone: { type: string, nullable: true }
        contact_mobile_phone: { type: string, nullable: true }
        is_email_unlocked: { type: boolean }
        is_phone_unlocked: { type: boolean }
    EnrichmentQuery:
      type: object
      description: >
        Provide exactly one valid combination: linkedin_url alone; email alone;
        or first_name + last_name + (company_name OR company_website).
      properties:
        linkedin_url: { type: string }
        email: { type: string, format: email }
        first_name: { type: string }
        last_name: { type: string }
        company_name: { type: string }
        company_website: { type: string }
    EnrichmentResult:
      type: object
      properties:
        success: { type: boolean }
        status: { type: string, enum: [success, not_found] }
        results:
          type: object
          properties:
            email: { type: string }
            phone: { type: string }
        message: { type: string }
        remaining_credits: { type: integer }
    ValidationRecord:
      type: object
      properties:
        id: { type: integer }
        ec_id: { type: string, nullable: true }
        email: { type: string }
        status: { type: string, enum: [Pending, Valid, Invalid, Unknown, Error] }
        validator_used:
          type: string
          nullable: true
          enum: [Primary, Secondary, SMTP, null]
        is_cached: { type: boolean }
        error_message: { type: string, nullable: true }
        processed_at: { type: string, format: date-time, nullable: true }
    ValidationJob:
      type: object
      properties:
        id: { type: integer }
        uuid: { type: string }
        list_name: { type: string }
        status: { type: string, enum: [Uploading, Processing, Processed, Failed] }
        total_records: { type: integer }
        valid_count: { type: integer }
        invalid_count: { type: integer }
        unknown_count: { type: integer }
        total_credits_charged: { type: number }
        credits_charged_at: { type: string, format: date-time }
        created_at: { type: string, format: date-time }
        started_at: { type: string, format: date-time, nullable: true }
        completed_at: { type: string, format: date-time, nullable: true }
        duration: { type: string }
        results_summary: { type: string }
        records:
          type: array
          items: { $ref: '#/components/schemas/ValidationRecord' }
    PaginatedJobs:
      type: object
      properties:
        count: { type: integer }
        total_pages: { type: integer }
        current_page: { type: integer }
        next: { type: string, nullable: true }
        previous: { type: string, nullable: true }
        results:
          type: array
          items: { $ref: '#/components/schemas/ValidationJob' }
    EnrichmentSubmit:
      type: object
      required: [type, level, fields_to_enrich]
      properties:
        type: { type: string, enum: [contact, account] }
        level:
          type: string
          enum: [standard, pro]
          description: account type accepts only 'standard'
        data:
          type: array
          description: 1–500 record objects (JSON mode)
          items: { type: object, additionalProperties: true }
        fields_to_enrich:
          type: array
          items: { type: string }
        criteria:
          type: object
          description: Account-only; activates contacts-per-account when limit is set
          properties:
            limit: { type: integer, minimum: 1 }
            job_level:
              oneOf:
                - { type: string }
                - { type: array, items: { type: string } }
            job_function:
              oneOf:
                - { type: string }
                - { type: array, items: { type: string } }
            keyword:
              oneOf:
                - { type: string }
                - { type: array, items: { type: string } }

paths:
  /external/search/:
    get:
      tags: [DataCore Search]
      summary: Search Records
      description: >
        Search the DataCore database with filters. PII fields (email, phone)
        are null until unlocked. Free — never charges credits.
      security:
        - tokenAuth: []
      parameters:
        - name: industry
          in: query
          description: >
            Parent or sub-industry name, exactly as listed in the Filter Values
            reference. Multiple industries can be combined.
          schema: { type: string, example: Technology }
        - { name: job_title, in: query, schema: { type: string }, example: Software Engineer }
        - name: job_level
          in: query
          schema:
            type: string
            enum:
              - Vice President Level
              - C Level
              - Director Level
              - Board Level
              - Executive Level
        - { name: company_name, in: query, schema: { type: string } }
        - { name: contact_office_country, in: query, schema: { type: string }, example: United States }
        - name: employee_range
          in: query
          schema:
            type: string
            enum: ["2-10", "11-50", "51-200", "201-500", "501-1,000", "1,001-5,000", "5,001-10,000", "10,001+"]
        - name: revenue_range
          in: query
          schema:
            type: string
            enum: ["$ <1M", "$ 1M - <10M", "$ 10M - <50M", "$ 50M - <100M", "$ 100M - <250M", "$ 250M - <500M", "$ 500M - <1B", "$ >1B"]
        - { name: keywords, in: query, schema: { type: string }, description: Free-text term; matched only against keyword_fields }
        - { name: keyword_fields, in: query, schema: { type: string }, description: Comma-separated fields the keywords term matches against, e.g. job_title,company_about_us }
        - { name: exclude_suppression, in: query, schema: { type: boolean }, description: Exclude contacts on your suppression lists }
        - { name: limit, in: query, schema: { type: integer, default: 10 } }
        - { name: offset, in: query, schema: { type: integer, default: 0 } }
      responses:
        "200":
          description: Matching records (PII hidden until unlocked)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  count: { type: integer }
                  results:
                    type: array
                    items: { $ref: '#/components/schemas/SearchRecord' }
        "401": { $ref: '#/components/responses/Unauthorized' }

  /external/unlock/:
    post:
      tags: [DataCore Search]
      summary: Unlock Records
      description: >
        Unlock email or phone data for records from Search. Email = 1 credit/record;
        phone = 5 credits/record (1 credit when no phone data on file); already-owned
        records are free.
      security:
        - tokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [uuids, unlock_type]
              properties:
                uuids:
                  type: array
                  items: { type: string }
                unlock_type:
                  type: string
                  enum: [email, phone]
      responses:
        "200":
          description: Unlock result
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  message: { type: string }
                  unlocked: { type: integer }
                  credits_charged: { type: integer }
                  wallet_balance: { type: integer }
        "400": { description: Missing uuids / invalid unlock_type }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "402": { $ref: '#/components/responses/PaymentRequired' }
        "404": { description: None of the supplied UUIDs exist }

  /pipeline/:
    post:
      tags: [Pipeline & Webhooks]
      summary: Submit Pipeline Job
      description: >
        Run search → suppress → unlock → verify → deliver as one tracked job.
        Returns a track_id. Hard ceiling of 10,000 records per job.
      security:
        - tokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [criteria]
              properties:
                criteria:
                  type: object
                  additionalProperties: true
                  description: Search filters — same keys as GET /external/search/ query params
                suppression_ids:
                  type: array
                  items: { type: string }
                unlock_email: { type: boolean }
                unlock_phone: { type: boolean }
                verify_emails: { type: boolean }
                max_records: { type: integer, maximum: 10000 }
      responses:
        "200":
          description: Job accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  track_id: { type: string }
                  status: { type: string, example: pending }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "402": { $ref: '#/components/responses/PaymentRequired' }

  /pipeline/{track_id}/:
    get:
      tags: [Pipeline & Webhooks]
      summary: Pipeline Status
      security:
        - tokenAuth: []
      parameters:
        - { name: track_id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Current job state
          content:
            application/json:
              schema:
                type: object
                properties:
                  job:
                    type: object
                    properties:
                      status: { type: string, enum: [pending, running, completed, failed] }
                      current_stage: { type: string }
                      progress: { type: integer }
                      matched_count: { type: integer }
                      unlocked_count: { type: integer }
                      verified_count: { type: integer }
                      credits_charged: { type: integer }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "404": { description: track_id not found for the caller }

  /pipeline/{track_id}/results/:
    get:
      tags: [Pipeline & Webhooks]
      summary: Pipeline Results
      description: Available once the job status is completed.
      security:
        - tokenAuth: []
      parameters:
        - { name: track_id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Delivered contacts
          content:
            application/json:
              schema:
                type: object
                properties:
                  count: { type: integer }
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        uuid: { type: string }
                        first_name: { type: string }
                        company_name: { type: string }
                        contact_email: { type: string }
                        email_status: { type: string }
                        email_valid: { type: boolean }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "404": { description: track_id not found for the caller }

  /webhooks/:
    post:
      tags: [Pipeline & Webhooks]
      summary: Register Webhook
      description: >
        Register a URL to receive job-completion pushes. The response includes a
        secret used to verify the HMAC-SHA256 X-Ecore-Signature on each delivery.
      security:
        - tokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url: { type: string, format: uri }
                events:
                  type: array
                  items: { type: string, enum: [pipeline.completed, pipeline.failed] }
      responses:
        "201":
          description: Webhook registered
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string }
                  url: { type: string }
                  events: { type: array, items: { type: string } }
                  secret: { type: string, description: Save this — used to verify delivery signatures }
        "401": { $ref: '#/components/responses/Unauthorized' }
    get:
      tags: [Pipeline & Webhooks]
      summary: List Webhooks
      security:
        - tokenAuth: []
      responses:
        "200": { description: Registered webhooks for the caller }
        "401": { $ref: '#/components/responses/Unauthorized' }

  /webhooks/{id}/:
    delete:
      tags: [Pipeline & Webhooks]
      summary: Delete Webhook
      security:
        - tokenAuth: []
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "204": { description: Deleted }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "404": { description: Webhook not found for the caller }

  /external/email/enrichment/:
    post:
      tags: [Email & Phone Enrichment]
      summary: Email Enrichment
      description: >
        Find an email by LinkedIn URL, existing email, or name + company.
        Charged 1 credit only on success; not_found is free.
      security:
        - tokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EnrichmentQuery' }
      responses:
        "200":
          description: Success or not_found (both 200)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EnrichmentResult' }
        "400": { description: No valid field combination supplied }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "402": { $ref: '#/components/responses/PaymentRequired' }

  /external/phone/enrichment/:
    post:
      tags: [Email & Phone Enrichment]
      summary: Phone Enrichment
      description: >
        Find a phone number by LinkedIn URL, existing email, or name + company.
        Charged 5 credits only on success; not_found is free. Also returns the
        matched email as a bonus when available.
      security:
        - tokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EnrichmentQuery' }
      responses:
        "200":
          description: Success or not_found (both 200)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EnrichmentResult' }
        "400": { description: No valid field combination supplied }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "402": { $ref: '#/components/responses/PaymentRequired' }

  /email-validation/jobs/upload/:
    post:
      tags: [Email Validation]
      summary: Upload Emails
      description: >
        Submit emails for validation via JSON list or CSV/Excel upload.
        Charged per email upfront; refunded for any email that is not Valid.
      security:
        - tokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [emails]
              properties:
                emails:
                  type: array
                  items: { type: string, format: email }
                list_name: { type: string, default: API Upload }
          multipart/form-data:
            schema:
              type: object
              required: [csv_file]
              properties:
                csv_file: { type: string, format: binary }
                list_name: { type: string }
      responses:
        "201":
          description: Job created
          content:
            application/json:
              schema:
                type: object
                properties:
                  job_id: { type: integer }
                  uuid: { type: string }
                  list_name: { type: string }
                  total_records: { type: integer }
                  status: { type: string, example: Uploading }
        "400": { description: No file/emails, bad format, or invalid addresses }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "402": { $ref: '#/components/responses/PaymentRequired' }

  /email-validation/jobs/:
    get:
      tags: [Email Validation]
      summary: List Jobs
      security:
        - tokenAuth: []
      parameters:
        - name: status
          in: query
          schema: { type: string, enum: [Uploading, Processing, Processed, Failed, All] }
        - { name: page, in: query, schema: { type: integer, default: 1 } }
        - { name: page_size, in: query, schema: { type: integer, default: 20, maximum: 100 } }
      responses:
        "200":
          description: Paginated job list
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PaginatedJobs' }
        "400": { description: Invalid status filter }
        "401": { $ref: '#/components/responses/Unauthorized' }

  /email-validation/jobs/{job_id}/:
    get:
      tags: [Email Validation]
      summary: Job Status
      description: Full details, aggregate stats, and the first 100 records inline.
      security:
        - tokenAuth: []
      parameters:
        - { name: job_id, in: path, required: true, schema: { type: integer } }
      responses:
        "200":
          description: Job detail
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ValidationJob' }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "404": { description: Not found or belongs to another user }

  /email-validation/jobs/{job_id}/records/:
    get:
      tags: [Email Validation]
      summary: Job Records
      security:
        - tokenAuth: []
      parameters:
        - { name: job_id, in: path, required: true, schema: { type: integer } }
        - name: status
          in: query
          schema: { type: string, enum: [Pending, Valid, Invalid, Unknown, Error, All] }
        - { name: page, in: query, schema: { type: integer, default: 1 } }
        - { name: page_size, in: query, schema: { type: integer, default: 100, maximum: 1000 } }
      responses:
        "200":
          description: Paginated per-record results
          content:
            application/json:
              schema:
                type: object
                properties:
                  count: { type: integer }
                  total_pages: { type: integer }
                  current_page: { type: integer }
                  next: { type: string, nullable: true }
                  previous: { type: string, nullable: true }
                  results:
                    type: array
                    items: { $ref: '#/components/schemas/ValidationRecord' }
        "400": { description: Invalid status filter }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "404": { description: Not found or belongs to another user }

  /email-validation/jobs/{job_id}/download/:
    get:
      tags: [Email Validation]
      summary: Download Results
      description: Bulk export of all records. Available only when status is Processed.
      security:
        - tokenAuth: []
      parameters:
        - { name: job_id, in: path, required: true, schema: { type: integer } }
        - name: format
          in: query
          schema: { type: string, enum: [csv, json], default: csv }
      responses:
        "200":
          description: CSV attachment or full JSON record list
          content:
            text/csv:
              schema: { type: string, format: binary }
            application/json:
              schema:
                type: array
                items: { $ref: '#/components/schemas/ValidationRecord' }
        "400": { description: Job is not yet complete }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "404": { description: Not found or belongs to another user }

  /enrichment/submit/:
    post:
      tags: [Account & Contact Enrichment]
      summary: Enrichment Submit
      description: >
        Submit a contact or account enrichment job (JSON records or CSV/Excel
        upload). Validates, prices, deducts estimated credits up front, and
        dispatches the async job. Returns a job_id immediately.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EnrichmentSubmit' }
          multipart/form-data:
            schema:
              type: object
              required: [file, type, level, fields_to_enrich]
              properties:
                file: { type: string, format: binary }
                type: { type: string, enum: [contact, account] }
                level: { type: string, enum: [standard, pro] }
                fields_to_enrich: { type: string, description: Comma-separated field names }
      responses:
        "200":
          description: Job queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: submitted }
                  job_id: { type: string, format: uuid }
                  estimated_credits: { type: integer }
                  message: { type: string }
        "400": { description: Validation failure (schema, fields, identifiers, plan limit, file parse) }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "402": { $ref: '#/components/responses/PaymentRequired' }
        "429": { description: Test-token daily usage cap exceeded }

  /enrichment/status/:
    get:
      tags: [Account & Contact Enrichment]
      summary: List Enrichment Jobs
      security:
        - bearerAuth: []
      responses:
        "200": { description: The caller's enrichment jobs, newest first }
        "401": { $ref: '#/components/responses/Unauthorized' }

  /enrichment/status/{job_id}/:
    get:
      tags: [Account & Contact Enrichment]
      summary: Enrichment Status
      security:
        - bearerAuth: []
      parameters:
        - { name: job_id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        "200":
          description: Current job status
          content:
            application/json:
              schema:
                type: object
                properties:
                  job_id: { type: string, format: uuid }
                  status:
                    type: string
                    enum: [queued, in-progress, completed, failed, paused]
                  submitted_at: { type: string, format: date-time }
                  updated_at: { type: string, format: date-time }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "404": { description: job_id not found for the caller }

  /enrichment/result/{job_id}/:
    get:
      tags: [Account & Contact Enrichment]
      summary: Enrichment Results
      description: Enriched records for a completed or paused job, as JSON pages or CSV.
      security:
        - bearerAuth: []
      parameters:
        - { name: job_id, in: path, required: true, schema: { type: string, format: uuid } }
        - name: report_type
          in: query
          schema: { type: string, enum: [json, csv], default: json }
        - { name: page, in: query, schema: { type: integer, default: 1 } }
        - { name: page_size, in: query, schema: { type: integer, default: 50, maximum: 200 } }
      responses:
        "200":
          description: Enriched records (JSON) or CSV download
          content:
            application/json:
              schema:
                type: object
                properties:
                  job_id: { type: string, format: uuid }
                  total_records: { type: integer }
                  page: { type: integer }
                  page_size: { type: integer }
                  data:
                    type: array
                    items: { type: object, additionalProperties: true }
            text/csv:
              schema: { type: string, format: binary }
        "400": { description: Status is not yet completed or paused }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "404": { description: job_id not found for the caller }

  /enrichment-control/:
    post:
      tags: [Account & Contact Enrichment]
      summary: Enrichment Control (Pause & Resume)
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [csv_file_id, action]
              properties:
                csv_file_id: { type: string, format: uuid, description: The job_id returned by Submit }
                action: { type: string, enum: [pause, resume] }
      responses:
        "200":
          description: Pause/resume acknowledged
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }
                  job_id: { type: string, format: uuid }
                  action: { type: string }
                  message: { type: string }
        "400": { description: Invalid action, wrong state, or missing/invalid checkpoint }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "402": { $ref: '#/components/responses/PaymentRequired' }
        "404": { description: job_id not found for the caller }

  /career-monitor/submit/:
    post:
      tags: [Career Monitor]
      summary: Submit Career Monitor Job
      description: >
        Check whether contacts are still at their company or have changed/left.
        Accepts a JSON list or a CSV/Excel upload. 5 credits per record (default);
        balance pre-checked at submit, deducted on completion. Test tokens are
        rejected (403).
      security:
        - tokenAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [data]
              properties:
                title: { type: string }
                data:
                  type: array
                  minItems: 1
                  items:
                    type: object
                    required: [company_name, linkedin_url]
                    properties:
                      company_name: { type: string, description: "or account_name / client_provided_name" }
                      linkedin_url: { type: string, description: "or website" }
                      ec_id: { type: string }
                      title: { type: string }
                      start_date: { type: string }
                      end_date: { type: string }
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: { type: string, format: binary, description: CSV or Excel; up to 10,000,000 rows / 5 GB }
                title: { type: string }
      responses:
        "200":
          description: Job accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: submitted }
                  job_id: { type: integer }
                  total_records: { type: integer }
                  estimated_credits: { type: number }
                  message: { type: string }
        "400": { description: Missing company/linkedin_url, bad/empty file, unsupported format, or too many rows }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "402": { $ref: '#/components/responses/PaymentRequired' }
        "403": { description: Test-mode token used }

  /career-monitor/jobs/:
    get:
      tags: [Career Monitor]
      summary: List Career Monitor Jobs
      security:
        - tokenAuth: []
      parameters:
        - { name: limit, in: query, schema: { type: integer, default: 10 } }
        - { name: offset, in: query, schema: { type: integer, default: 0 } }
      responses:
        "200":
          description: Paginated job list (newest first)
          content:
            application/json:
              schema:
                type: object
                properties:
                  count: { type: integer }
                  next: { type: string, nullable: true }
                  previous: { type: string, nullable: true }
                  results:
                    type: array
                    items: { $ref: '#/components/schemas/CareerMonitorJob' }
        "401": { $ref: '#/components/responses/Unauthorized' }

  /career-monitor/jobs/{job_id}/:
    get:
      tags: [Career Monitor]
      summary: Career Monitor Job Status
      security:
        - tokenAuth: []
      parameters:
        - { name: job_id, in: path, required: true, schema: { type: integer } }
      responses:
        "200":
          description: Job detail
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CareerMonitorJob' }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "404": { description: Job not found or not owned by this account }

  /career-monitor/result/{job_id}/:
    get:
      tags: [Career Monitor]
      summary: Career Monitor Results
      description: Paginated JSON or CSV export. Can be polled while still Processing.
      security:
        - tokenAuth: []
      parameters:
        - { name: job_id, in: path, required: true, schema: { type: integer } }
        - name: report_type
          in: query
          schema: { type: string, enum: [json, csv], default: json }
        - { name: page, in: query, schema: { type: integer, default: 1 } }
        - { name: page_size, in: query, schema: { type: integer, default: 100, maximum: 1000 } }
      responses:
        "200":
          description: Job results (JSON) or CSV export
          content:
            application/json:
              schema:
                type: object
                properties:
                  job_id: { type: integer }
                  job_status: { type: string }
                  total_records: { type: integer }
                  page: { type: integer }
                  page_size: { type: integer }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/CareerMonitorRecord' }
            text/csv:
              schema: { type: string, format: binary }
        "400": { description: Job not found or not owned by this account }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "403": { description: Test-mode token used }

  # NOTE: docs backlog cleared — filter enums (job_level, employee_range,
  # revenue_range), industry taxonomy, job_function common values, per-row
  # enrichment prices, no-rate-limit on enrichment submit/status/results, and
  # catch-all handling (validation statuses valid/invalid/unknown; enrichment
  # bundles the deliverability check at no separate charge) are all documented.
