> ## 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.

# Disconnect or delete a number

> DELETE /v1/instances/{id} — sign WhatsApp out, or remove the instance entirely.

One endpoint, two very different outcomes. The `logout_only` query parameter decides which.

<Warning>
  **Read this before you call it.** Deleting is permanent: the `instance_id` stops working forever, and anything in your code holding it will start returning `404 instance_not_found`.
</Warning>

## The two modes

<Columns cols={2}>
  <Card title="Disconnect — logout_only=true" icon="plug-circle-minus">
    Signs WhatsApp out but **keeps the instance**.

    * `instance_id` stays valid
    * Reconnect by scanning again
    * Nothing to change in your code

    Returns `{ "status": "close", "disconnected": true }`.
  </Card>

  <Card title="Delete — no parameter" icon="trash">
    Removes the instance **permanently**.

    * `instance_id` stops working
    * You would have to create a new one and redeploy
    * Frees the slot on your account

    Returns `{ "deleted": true }`.
  </Card>
</Columns>

## Exactly what each value does

The flag is never guessed at. A value we cannot read is **refused**, and nothing on your account changes.

| You send                                                                    | What happens                                                            |
| --------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `logout_only=true`, `=1`, `=yes`, `=on` (any capitalisation)                | **Disconnect.** The instance and its `instance_id` survive.             |
| `logout_only=false`, `=0`, `=no`, `=off`                                    | **Delete permanently** — you asked explicitly.                          |
| No `logout_only` parameter at all                                           | **Delete permanently** — the long-standing default for a bare `DELETE`. |
| Any other value — `=2`, `=y`, `=maybe`, the literal `undefined`             | **`400 invalid_request`. Nothing is deleted, nothing is disconnected.** |
| A bare `?logout_only` with no value                                         | `400 invalid_request`. Nothing changes.                                 |
| The parameter sent twice (`?logout_only=true&logout_only=false`)            | `400 invalid_request`. Nothing changes.                                 |
| A misspelled name — `?logoutOnly=true`, `?logout-only=true`, `?logout=true` | `400 invalid_request` telling you the correct name. Nothing changes.    |

<Check>
  If you are building the query string from a variable, this is the case that matters: `?logout_only=${flag}` with an undefined `flag` sends the literal text `undefined`, and that now returns a `400` instead of destroying the instance.
</Check>

## Which one do you want?

| Situation                                              | Use                              |
| ------------------------------------------------------ | -------------------------------- |
| The number keeps dropping and you want a clean re-pair | `logout_only=true`, then re-scan |
| You are handing the phone to someone else temporarily  | `logout_only=true`               |
| You are done with this number for good                 | Delete                           |
| You need the slot for a different number               | Delete                           |

<Tip>
  If a number has simply gone offline, you usually need **neither**. Just re-scan the QR on the existing instance — see [If your number disconnects](/guides/disconnects).
</Tip>

## Examples

```bash Disconnect, keep the instance theme={null}
curl -X DELETE "https://api.bulkneo.com/v1/instances/YOUR_INSTANCE_ID?logout_only=true" \
  -H "apikey: YOUR_API_KEY"
```

```bash Delete permanently theme={null}
curl -X DELETE "https://api.bulkneo.com/v1/instances/YOUR_INSTANCE_ID" \
  -H "apikey: YOUR_API_KEY"
```

```javascript Node.js — never let a falsy variable decide theme={null}
// Send the parameter only when you actually mean "disconnect". Do not
// interpolate a variable into the query string and hope it renders as "true".
const url = new URL(`https://api.bulkneo.com/v1/instances/${instanceId}`);
if (disconnectOnly) url.searchParams.set("logout_only", "true");

const res = await fetch(url, {
  method: "DELETE",
  headers: { apikey: process.env.BULKNEO_API_KEY },
});
```

<Note>
  Deleting or disconnecting a number does not credit anything back for the day it was already active. Reconnecting the same number later the same day costs nothing extra.
</Note>


## OpenAPI

````yaml api-reference/openapi.json DELETE /v1/instances/{id}
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/instances/{id}:
    delete:
      tags:
        - Instances
      summary: Disconnect or delete a number
      description: >-
        With `logout_only=true`, signs WhatsApp out but KEEPS the instance, so
        the same `instance_id` can be reconnected by scanning again. With no
        `logout_only` parameter at all — or an explicit `logout_only=false` —
        the instance is deleted permanently and its `instance_id` stops working,
        so you would have to create a new one and update your integration.


        A `logout_only` value that is not one of the accepted spellings is
        REFUSED with `400 invalid_request` and nothing is changed. The parameter
        is never guessed at: an unrecognised value can no longer fall through to
        the delete.
      operationId: deleteInstance
      parameters:
        - $ref: '#/components/parameters/InstanceIdPath'
        - name: logout_only
          in: query
          required: false
          schema:
            type: string
            enum:
              - 'true'
              - '1'
              - 'yes'
              - 'on'
              - 'false'
              - '0'
              - 'no'
              - 'off'
          description: >-
            `true`, `1`, `yes` or `on` (any capitalisation) disconnect WhatsApp
            and KEEP the instance. `false`, `0`, `no` or `off` delete it, as
            does omitting the parameter entirely. Any other value — including an
            empty one, and the parameter sent twice — returns `400
            invalid_request` and changes nothing.
      responses:
        '200':
          description: Disconnected or deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    const: true
                  data:
                    type: object
                    properties:
                      instance_id:
                        type: string
                        format: uuid
                      status:
                        type: string
                        description: Present on a logout only. Always `close`.
                      disconnected:
                        type: boolean
                        description: Present and `true` on a logout.
                      deleted:
                        type: boolean
                        description: Present and `true` on a delete.
                  request_id:
                    type: string
                    format: uuid
              example:
                success: true
                data:
                  instance_id: 3f9c1a2b-7d4e-4c81-9f0a-2b6d5e8c1a34
                  deleted: true
                request_id: b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/InstanceNotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '502':
          $ref: '#/components/responses/UpstreamUnavailable'
components:
  parameters:
    InstanceIdPath:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: The `instance_id` of one of your numbers.
      example: 3f9c1a2b-7d4e-4c81-9f0a-2b6d5e8c1a34
  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
    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
    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
  schemas:
    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.
  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.

````