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

# Check numbers

> POST /v1/numbers/check — which of these numbers are on WhatsApp?

Ask which numbers are reachable on WhatsApp before you send to them. Nothing is sent to anybody.

## Notes

* **1 to 50 numbers per request.** More returns `400 invalid_request` with `details.max_numbers`.
* Duplicates are removed before the lookup. `submitted` is what you sent, `checked` is how many distinct numbers were actually looked up.
* Results come back **in the order you submitted them**.
* This uses one of your connected numbers to do the lookup, so `instance_id` is required and that number must be connected — otherwise you get `409 instance_not_connected`.
* A check is **not a message**. It does not appear in your usage figures and does not consume a trial allowance. It does count against your per-minute [rate limit](/errors#rate-limits).

## Reading the results

<ResponseField name="exists" type="boolean | null">
  `true` — on WhatsApp. `false` — not on WhatsApp. **`null` — the check could not be completed for that number.**
</ResponseField>

<Warning>
  `null` does **not** mean "not on WhatsApp". Treat it as "unknown" and retry later, or send anyway. Collapsing `null` into `false` will make you skip real customers.
</Warning>

<ResponseField name="whatsapp_number" type="string | null">
  The number WhatsApp actually answers on. In some countries this differs from the number you dialled — Brazil's extra 9th digit is the classic case. When it differs, **send to this number**, not the one you looked up.
</ResponseField>

## Example

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.bulkneo.com/v1/numbers/check \
    -H "apikey: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "instance_id": "YOUR_INSTANCE_ID",
      "numbers": ["919876543210", "919812345678"]
    }'
  ```

  ```javascript Node.js theme={null}
  const res = await fetch("https://api.bulkneo.com/v1/numbers/check", {
    method: "POST",
    headers: {
      apikey: process.env.BULKNEO_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      instance_id: process.env.BULKNEO_INSTANCE_ID,
      numbers: batch, // at most 50
    }),
  });

  const { data } = await res.json();
  const sendable = data.results
    .filter((r) => r.exists === true)
    .map((r) => r.whatsapp_number ?? r.number);
  ```

  ```python Python theme={null}
  import os
  import requests

  res = requests.post(
      "https://api.bulkneo.com/v1/numbers/check",
      headers={"apikey": os.environ["BULKNEO_API_KEY"]},
      json={
          "instance_id": os.environ["BULKNEO_INSTANCE_ID"],
          "numbers": batch,  # at most 50
      },
      timeout=30,
  )
  res.raise_for_status()

  results = res.json()["data"]["results"]
  sendable = [r["whatsapp_number"] or r["number"] for r in results if r["exists"] is True]
  unknown = [r["number"] for r in results if r["exists"] is None]
  ```
</CodeGroup>

<Tip>
  Working through a large list quickly still puts your number at risk, whatever the batch size. Read [Checking numbers before you send](/guides/checking-numbers) for how to pace it.
</Tip>


## OpenAPI

````yaml api-reference/openapi.json POST /v1/numbers/check
openapi: 3.1.0
info:
  title: BulkNeo WhatsApp API
  version: 1.0.0
  description: >-
    Send WhatsApp messages from your own connected numbers. Every request is
    authenticated with an `apikey` header and names the `instance_id` of the
    number it should be sent from.
servers:
  - url: https://api.bulkneo.com
    description: BulkNeo API
security:
  - apikey: []
tags:
  - name: Messages
    description: Send a message from one of your connected numbers.
  - name: Numbers
    description: Check which numbers are reachable on WhatsApp.
  - name: Instances
    description: Create, connect, inspect and remove your WhatsApp numbers.
  - name: Service
    description: Unauthenticated service health checks.
paths:
  /v1/numbers/check:
    post:
      tags:
        - Numbers
      summary: Check numbers on WhatsApp
      description: >-
        Asks which of the given numbers are reachable on WhatsApp. Nothing is
        sent to them. Results come back in the order you submitted, and
        duplicates are collapsed before the lookup.
      operationId: checkNumbers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - instance_id
                - numbers
              properties:
                instance_id:
                  $ref: '#/components/schemas/InstanceId'
                numbers:
                  type: array
                  minItems: 1
                  maxItems: 50
                  description: >-
                    1 to 50 numbers per request, each with its country code.
                    Duplicates are removed before the lookup.
                  items:
                    type: string
                  example:
                    - '919876543210'
                    - '919812345678'
            example:
              instance_id: 3f9c1a2b-7d4e-4c81-9f0a-2b6d5e8c1a34
              numbers:
                - '919876543210'
                - '919812345678'
      responses:
        '200':
          description: Lookup complete.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      instance_id:
                        type: string
                        format: uuid
                      submitted:
                        type: integer
                        description: How many numbers you sent, including duplicates.
                      checked:
                        type: integer
                        description: How many distinct numbers were actually looked up.
                      results:
                        type: array
                        items:
                          type: object
                          properties:
                            number:
                              type: string
                              description: The number as normalised from your input.
                            exists:
                              type:
                                - boolean
                                - 'null'
                              description: >-
                                `true` on WhatsApp, `false` not on WhatsApp,
                                `null` the check could not be completed for this
                                number — which does NOT mean the number is
                                absent.
                            whatsapp_number:
                              type:
                                - string
                                - 'null'
                              description: >-
                                The number WhatsApp actually answers on. In some
                                countries this differs from the number you
                                dialled — send to this one.
                  request_id:
                    type: string
                    format: uuid
              example:
                success: true
                data:
                  instance_id: 3f9c1a2b-7d4e-4c81-9f0a-2b6d5e8c1a34
                  submitted: 2
                  checked: 2
                  results:
                    - number: '919876543210'
                      exists: true
                      whatsapp_number: '919876543210'
                    - number: '919812345678'
                      exists: false
                      whatsapp_number: null
                request_id: b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/InstanceNotFound'
        '409':
          $ref: '#/components/responses/NotConnected'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '502':
          $ref: '#/components/responses/UpstreamUnavailable'
components:
  schemas:
    InstanceId:
      type: string
      format: uuid
      description: >-
        Which of your connected numbers to send FROM. Copy it from your numbers
        screen in the portal, or from `GET /v1/instances`. It must belong to
        your account.
      example: 3f9c1a2b-7d4e-4c81-9f0a-2b6d5e8c1a34
    Error:
      type: object
      properties:
        success:
          type: boolean
          const: false
        error:
          type: object
          properties:
            code:
              type: string
              description: >-
                Stable machine-readable code. Branch on this, not on the
                message.
            message:
              type: string
              description: Human-readable explanation. The wording may change.
            details:
              type: object
              description: Extra safe context, present on some errors.
        request_id:
          type: string
          format: uuid
          description: Quote this when asking support about a request.
  responses:
    BadRequest:
      description: >-
        `invalid_request` a field is missing or malformed (the message names it,
        including its position in a list) · `invalid_media_url` the `url` is not
        a public http(s) link to a file.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            success: false
            error:
              code: invalid_request
              message: Field "text" is required and must be a non-empty string.
            request_id: b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d
    Unauthorized:
      description: >-
        `missing_api_key` no `apikey` header was sent · `invalid_api_key` the
        key is unknown or revoked.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            success: false
            error:
              code: invalid_api_key
              message: Invalid or revoked API key.
            request_id: b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d
    PaymentRequired:
      description: >-
        `no_active_subscription` no usable plan on this account ·
        `subscription_expired` the plan has run out.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            success: false
            error:
              code: subscription_expired
              message: >-
                Your subscription has expired. Renew it to continue sending
                messages.
            request_id: b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d
    Forbidden:
      description: >-
        `account_suspended` · `access_expired` · `trial_quota_exceeded` ·
        `instance_limit_reached` · `activation_unavailable`.


        On `instance_limit_reached`, `details.limit_source` tells you which cap
        you hit: `plan` (the plan you are on — upgrade it) or `account` (a cap
        set on your account — ask your provider to raise it). Branch on that,
        never on the wording of the message.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            success: false
            error:
              code: trial_quota_exceeded
              message: >-
                Trial message quota reached (500 messages). Upgrade to a paid
                plan to keep sending.
              details:
                trial_message_limit: 500
            request_id: b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d
    InstanceNotFound:
      description: >-
        `instance_not_found` — no such instance on your account. The same answer
        is given for an id that does not exist and one that belongs to someone
        else.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            success: false
            error:
              code: instance_not_found
              message: >-
                Instance not found. Check the instance_id and that it belongs to
                your account.
            request_id: b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d
    NotConnected:
      description: >-
        `instance_not_connected` — the number is not connected. Do not retry:
        pause and have someone re-scan the QR.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            success: false
            error:
              code: instance_not_connected
              message: >-
                This WhatsApp instance is not connected. Connect it before
                sending messages.
            request_id: b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d
    RateLimited:
      description: >-
        `rate_limit_exceeded` — too many requests this minute. Wait for the
        number of seconds in the `Retry-After` header. `details.scope` and the
        `X-RateLimit-Scope` header say WHICH allowance you exceeded.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
        X-RateLimit-Scope:
          $ref: '#/components/headers/RateLimitScope'
        X-RateLimit-Limit:
          $ref: '#/components/headers/RateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/RateLimitRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/RateLimitReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            success: false
            error:
              code: rate_limit_exceeded
              message: Rate limit exceeded, retry in 12s.
              details:
                retry_after_seconds: 12
                limit_per_minute: 20
                scope: messages
            request_id: b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d
    InternalError:
      description: >-
        `internal_error` — something went wrong on our side. Quote the
        `request_id` to support.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            success: false
            error:
              code: internal_error
              message: An unexpected error occurred.
            request_id: b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d
    UpstreamUnavailable:
      description: >-
        `upstream_unavailable` — the messaging service is temporarily
        unavailable. Safe to retry after a short pause.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            success: false
            error:
              code: upstream_unavailable
              message: >-
                The messaging service is temporarily unavailable. Please retry
                shortly.
            request_id: b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d
  headers:
    RateLimitScope:
      description: >-
        WHICH allowance the other X-RateLimit-* headers describe. `messages`
        covers every /v1/messages/* endpoint and /v1/numbers/check together;
        `instances` is the separate, much larger budget for /v1/instances/*. If
        you cache a limit, cache it against its scope.
      schema:
        type: string
        enum:
          - messages
          - instances
      example: messages
    RateLimitLimit:
      description: Requests allowed per minute on this scope.
      schema:
        type: integer
      example: 20
    RateLimitRemaining:
      description: Requests left in the current minute on this scope.
      schema:
        type: integer
      example: 17
    RateLimitReset:
      description: >-
        Seconds until the allowance next goes up by one. Present on successful
        responses too, so you can pace yourself without first earning a 429. The
        window is a rolling minute, so this is when the oldest request ages out
        — not a fixed reset instant. If `X-RateLimit-Remaining` is above 0 you
        can send now.
      schema:
        type: integer
      example: 43
  securitySchemes:
    apikey:
      type: apiKey
      in: header
      name: apikey
      description: >-
        Your API key, sent in an `apikey` request header. Never in the URL,
        never as a Bearer token.

````