> ## Documentation Index
> Fetch the complete documentation index at: https://docs.airscale.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Create a job-change monitor

> Monitor up to 10,000 personal LinkedIn profiles for employer changes with one schedule, one results table and an optional webhook. Rate limit: 120 requests per fixed minute per API key, shared by all job-change monitor endpoints.

> [!NOTE]
>
> Job-change monitoring requires a **V2 workspace API key**. V1 (Bubble) API keys are not supported by these endpoints. Monitors and monitoring credits belong to the V2 workspace associated with the key.

Create an active monitor for one to 10,000 personal LinkedIn profiles with [Bearer authentication](https://docs.airscale.io/api-reference/authentication.md). All profiles belong to one monitor ID, with one frequency, one results table and an optional webhook. Each successful request creates a new monitor, and creation has no idempotency key. If a network failure leaves the result unknown, [list your monitors](https://docs.airscale.io/api-reference/job-change-monitors/list.md) and reconcile before retrying; blindly retrying can create a second monitor with separate charges.

> [!NOTE]
>
> The `frequency` can be `weekly`, `every_30_days`, or `every_90_days` (default `weekly`). If you provide `webhook_url`, store the returned `signing_secret` immediately; it is returned only once. If you lose it, [rotate it](https://docs.airscale.io/api-reference/job-change-monitors/update.md) on the existing monitor instead of creating a new one.

Airscale canonicalizes LinkedIn URLs, which must identify a personal `/in/` profile, and preserves each profile's optional `external_id` in later events. Monitoring admission checks that job-change monitoring is available and that the workspace can pay for a full check before accepting the monitor.

## Monitor 10,000 profiles

1. Prepare a JSON request with `name`, your chosen `frequency`, an optional `webhook_url`, and a `profiles` array containing up to 10,000 personal LinkedIn URLs. Each entry can include an `external_id` from your CRM.
2. Send it to `POST /v1/job-change-monitors`. The `202 Accepted` response contains `monitor`, `profiles`, `duplicates`, `skipped_duplicate_count`, `signing_secret`, and `signing_secret_warning`. Save `monitor.id` and, if a webhook is configured, the one-time `signing_secret`.
3. Use that same ID with [Get a monitor](https://docs.airscale.io/api-reference/job-change-monitors/get.md) to inspect profile statuses and the monitor's check timestamps. Checks run asynchronously and a large check can take time; `202` confirms acceptance, not completion.
4. After a profile's first successful check establishes its baseline, later employer changes appear in [List job-change events](https://docs.airscale.io/api-reference/job-change-monitors/events.md), are appended to the monitor's results table, and are sent to the configured webhook. The first baseline does not emit a job-change event.

Monitoring costs **0.1 credit per profile per check**. A full check of 10,000 profiles costs up to 1,000 credits. Failed, not-found, and no-current-employer lookups are refunded. A check the workspace cannot pay for in full is not started: the monitor then reports `run_block_reason: "insufficient_credits"` with `run_block_detail` until a later check starts.

## Results table

Each monitor owns a regular Airscale results table. Every new job-change event appends a row; the initial baseline does not. The monitor reports `results_table_id`, `results_status` (`pending`, `ready`, or `blocked`) and `results_error`. If the table is deleted or full, new rows stay pending and `results_error` explains why. Deleting the monitor keeps its results table.

## Profiles already watched by another monitor

`duplicates` lists profiles that another monitor in the workspace already watches, with `linkedin_url`, `monitor_id`, and `monitor_name`. Each monitor checks and charges its profiles separately. Send `"skip_duplicates": true` to leave them out; `skipped_duplicate_count` reports how many were skipped. A request whose every profile is a duplicate is rejected with `422 all_profiles_duplicated`.

## Request size and profile limits

One monitor supports at most **10,000 active profiles**. Create and [Add profiles](https://docs.airscale.io/api-reference/job-change-monitors/profiles/add.md) each accept up to **10,000 profile entries per request**, while the monitor's resulting active count must stay within 10,000.

The complete JSON request body must be at most **8 MiB (8,388,608 bytes)** in UTF-8. A larger body returns `413`. Long URLs, `external_id` values, and JSON formatting all count toward this limit. If your payload is too large, create the monitor with a smaller set, then add the remaining profiles in smaller requests to the same `monitor.id`.

## Webhook URL rules

`webhook_url` must be a public HTTPS URL without embedded credentials. Other protocols, internal hostnames, and private or reserved addresses are rejected with `422 invalid_monitor` before the monitor is created. Destinations are checked again when sending; a hostname that later resolves to a blocked address cannot receive deliveries.

## Verify webhook signatures

Airscale sends each event as a JSON `POST` to `webhook_url`. The body has `type: "job_change.detected"` plus the same fields as an event from [List job-change events](https://docs.airscale.io/api-reference/job-change-monitors/events.md) without `read_at` or `webhook`: `id`, `monitor_id`, `profile_id`, `linkedin_url`, `external_id`, `detected_at`, `previous_employer`, and `new_employer` (each with `identity` and `name`).

The request includes `X-Airscale-Event: job_change.detected`, `X-Airscale-Event-Id`, `X-Airscale-Timestamp`, and `X-Airscale-Signature: sha256=<hex>`. Compute HMAC-SHA256 with the `signing_secret` over `<timestamp>.<raw request body>`, compare it in constant time, and reject timestamps more than five minutes from the current time. Return a 2xx response only after durable handling.

Airscale makes up to five attempts with exponential backoff. `408`, `425`, `429`, and `5xx` responses are retried; other definitive responses fail permanently, and timeouts or dropped connections are marked `uncertain` because the request may have been received. Retries keep the same event ID and send `Idempotency-Key: job-change-event:<event_id>`, so process events idempotently. Use [Redeliver a webhook](https://docs.airscale.io/api-reference/job-change-monitors/events/redeliver.md) to send a failed or uncertain event again.

## Next step

Use [List job-change monitors](https://docs.airscale.io/api-reference/job-change-monitors/list.md) to confirm the monitor's status and next check time.


## OpenAPI

````yaml openapi.json POST /v1/job-change-monitors
openapi: 3.1.0
info:
  title: Airscale Public API
  version: '2026-09-11'
  description: >-
    Search, enrich, resolve public business data, and monitor job changes with
    Airscale.
  x-airscale-source-repository: ViceScale/airscale-code
  x-airscale-source-sha: 1de19e1b70a052a4b8d9c2075021a7e5e7a94d51
servers:
  - url: https://api.airscale.io
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Search and discovery
    description: Search people, companies, and the web.
  - name: Contact data
    description: Find professional and personal contact data.
  - name: Profiles and reverse lookup
    description: Extract profiles or resolve a person from known contact data.
  - name: Post engagement
    description: Retrieve and enrich people who liked or commented on LinkedIn posts.
  - name: LinkedIn content
    description: >-
      Search LinkedIn posts, read profile and company feeds, and retrieve
      profile comments and comment reactions.
  - name: Job change monitoring
    description: Track LinkedIn profiles and receive job-change events.
  - name: Account
    description: Inspect workspace account state.
  - name: Miscellaneous
    description: >-
      Find website technologies and check WhatsApp availability, Meta ads, and
      email deliverability.
paths:
  /v1/job-change-monitors:
    post:
      tags:
        - Job change monitoring
      summary: Create a job-change monitor
      description: >-
        Creates a new active monitor for 1–10,000 personal LinkedIn profiles
        with one frequency and an optional webhook. Each monitor also owns an
        Airscale results table that receives one row per new job-change event.
        Checks run asynchronously; the first successful check of a profile
        establishes its baseline without emitting an event. If webhook_url is
        set, the 202 response includes a signing_secret exactly once. Every
        request creates a new monitor and there is no idempotency key: if the
        response is lost, list monitors before retrying. Requires a V2 workspace
        API key. V1 (Bubble) API keys are not supported. Monitors and monitoring
        credits belong to the V2 workspace.
      operationId: createJobChangeMonitor
      requestBody:
        required: true
        description: >-
          Choose a check frequency and provide 1–10,000 personal LinkedIn
          profiles in a JSON body of at most 8 MiB (8,388,608 bytes). For larger
          payloads, create with fewer profiles and add the remainder to the same
          monitor in smaller requests.
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - name
                - profiles
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 120
                frequency:
                  type: string
                  enum:
                    - weekly
                    - every_30_days
                    - every_90_days
                  default: weekly
                webhook_url:
                  type:
                    - string
                    - 'null'
                  format: uri
                  maxLength: 2048
                  description: >-
                    A public HTTPS URL without embedded credentials. Internal
                    hostnames and private or reserved addresses are rejected.
                skip_duplicates:
                  type: boolean
                  default: false
                  description: >-
                    Leave out profiles that another monitor in this workspace
                    already watches. Without it, duplicates are added and
                    reported in duplicates.
                profiles:
                  type: array
                  minItems: 1
                  maxItems: 10000
                  items:
                    type: object
                    additionalProperties: false
                    required:
                      - linkedin_url
                    properties:
                      linkedin_url:
                        type: string
                        format: uri
                        minLength: 1
                        maxLength: 512
                        description: >-
                          A personal LinkedIn profile URL. Airscale
                          canonicalizes it to
                          https://www.linkedin.com/in/{slug}.
                      external_id:
                        type: string
                        minLength: 1
                        maxLength: 200
                        description: >-
                          Your CRM or application identifier. It is echoed in
                          profile records and job-change events.
            examples:
              monitor:
                summary: Monitor with one synthetic profile
                value:
                  name: CRM champions
                  frequency: every_30_days
                  webhook_url: https://hooks.example.com/airscale/job-changes
                  profiles:
                    - linkedin_url: https://www.linkedin.com/in/example-person
                      external_id: crm-contact-123
      responses:
        '202':
          description: >-
            The monitor was accepted and scheduled. Checks run asynchronously;
            acceptance does not mean a check has completed.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - monitor
                  - profiles
                  - duplicates
                  - skipped_duplicate_count
                  - signing_secret
                  - signing_secret_warning
                properties:
                  monitor:
                    $ref: '#/components/schemas/JobChangeMonitor'
                  profiles:
                    type: array
                    items:
                      $ref: '#/components/schemas/JobChangeProfile'
                  duplicates:
                    type: array
                    items:
                      $ref: '#/components/schemas/JobChangeDuplicate'
                    description: >-
                      Profiles that another monitor in this workspace already
                      watches. Each monitor checks and charges its profiles
                      separately.
                  skipped_duplicate_count:
                    type: integer
                    minimum: 0
                  signing_secret:
                    type:
                      - string
                      - 'null'
                    description: >-
                      Returned once when a webhook is configured; null
                      otherwise.
                  signing_secret_warning:
                    type:
                      - string
                      - 'null'
              examples:
                success:
                  summary: Successful response
                  value:
                    monitor:
                      id: 11111111-1111-4111-8111-111111111111
                      name: CRM champions
                      frequency: every_30_days
                      status: active
                      results_table_id: 44444444-4444-4444-8444-444444444444
                      results_status: ready
                      results_error: null
                      next_check_at: '2026-11-02T10:00:00.000Z'
                      last_successful_check_at: null
                      created_at: '2026-11-02T10:00:00.000Z'
                      updated_at: '2026-11-02T10:00:00.000Z'
                      webhook_url: https://hooks.example.com/airscale/job-changes
                      webhook_configured: true
                      run_block_reason: null
                      run_block_detail: null
                      active_profile_count: 1
                      unread_event_count: 0
                    profiles:
                      - id: 22222222-2222-4222-8222-222222222222
                        linkedin_url: https://www.linkedin.com/in/example-person
                        external_id: crm-contact-123
                        status: pending
                        removed_at: null
                    duplicates: []
                    skipped_duplicate_count: 0
                    signing_secret: example-signing-secret
                    signing_secret_warning: Store this secret now. It is not returned again.
        '400':
          description: >-
            The request body is missing or is not valid JSON. The public API
            returns {"error":"Invalid JSON"}.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: >-
            insufficient_credits: the workspace cannot pay for the next full
            check of the monitor's active profiles.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobChangeError'
        '413':
          description: The JSON request body exceeds the 8 MiB (8,388,608 bytes) limit.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: >-
            invalid_monitor (for example an invalid LinkedIn URL, duplicate URLs
            in the request, or a webhook_url that is not a public HTTPS
            destination), or all_profiles_duplicated when skip_duplicates leaves
            no profiles.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobChangeError'
        '429':
          description: >-
            The API key has exceeded 120 requests in the current minute across
            job-change monitor endpoints. Wait for the Retry-After seconds
            before retrying.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          headers:
            Retry-After:
              description: Seconds to wait before retrying.
              schema:
                type: string
              example: '60'
        '503':
          description: Job change monitoring is temporarily unavailable or not enabled.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobChangeError'
components:
  schemas:
    JobChangeMonitor:
      type: object
      additionalProperties: false
      required:
        - id
        - name
        - frequency
        - status
        - results_table_id
        - results_status
        - results_error
        - next_check_at
        - last_successful_check_at
        - created_at
        - updated_at
        - webhook_url
        - webhook_configured
        - run_block_reason
        - run_block_detail
        - active_profile_count
        - unread_event_count
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        frequency:
          type: string
          enum:
            - weekly
            - every_30_days
            - every_90_days
        status:
          type: string
          enum:
            - active
            - paused
            - running
            - setup_failed
        results_table_id:
          type:
            - string
            - 'null'
          description: >-
            The Airscale results table that receives one row per new job-change
            event. Null until the table exists.
        results_status:
          type: string
          enum:
            - pending
            - ready
            - blocked
          description: >-
            Whether the results table can receive rows. blocked means the table
            was deleted or is full; new rows stay pending.
        results_error:
          type:
            - string
            - 'null'
          description: Why results delivery is blocked, or null.
        next_check_at:
          type:
            - string
            - 'null'
          format: date-time
        last_successful_check_at:
          type:
            - string
            - 'null'
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        webhook_url:
          type:
            - string
            - 'null'
          format: uri
        webhook_configured:
          type: boolean
        run_block_reason:
          type:
            - string
            - 'null'
          description: >-
            Why the latest check could not start or stopped early, for example
            insufficient_credits. Null when no block is active.
        run_block_detail:
          type:
            - object
            - 'null'
          additionalProperties: true
          description: >-
            Details for run_block_reason, such as stage, credits_needed and
            credits_available, or the checked and skipped counts of a run that
            stopped early.
        active_profile_count:
          type: integer
          minimum: 0
          maximum: 10000
        unread_event_count:
          type: integer
          minimum: 0
    JobChangeProfile:
      type: object
      additionalProperties: false
      required:
        - id
        - linkedin_url
        - external_id
        - status
        - removed_at
      properties:
        id:
          type: string
          format: uuid
        linkedin_url:
          type: string
          format: uri
        external_id:
          type:
            - string
            - 'null'
        status:
          type: string
          enum:
            - pending
            - baseline
            - checked
            - unavailable
            - failed
            - removed
        removed_at:
          type:
            - string
            - 'null'
          format: date-time
    JobChangeDuplicate:
      type: object
      additionalProperties: false
      required:
        - linkedin_url
        - monitor_id
        - monitor_name
      properties:
        linkedin_url:
          type: string
          format: uri
        monitor_id:
          type: string
          format: uuid
        monitor_name:
          type: string
    Error:
      type: object
      additionalProperties: true
      properties:
        error:
          type: string
        message:
          type: string
    JobChangeError:
      type: object
      additionalProperties: false
      required:
        - error
      properties:
        error:
          type: object
          additionalProperties: false
          required:
            - code
            - message
          properties:
            code:
              type: string
            message:
              type: string
  responses:
    Unauthorized:
      description: The Bearer token is missing or invalid.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API key
      description: >-
        Use an Airscale workspace API key. Never expose the key in client-side
        code.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.