{
  "openapi": "3.1.0",
  "info": {
    "title": "WAChecker / BulkNumberChecker Public API",
    "version": "1.0.0",
    "description": "Free, no-auth REST API to check whether phone numbers are registered on WhatsApp.\n\n**Free tier limits:**\n- 10 numbers per request\n- 5 requests per IP per day (resets at UTC midnight)\n- Poll endpoints (`GET`) are unmetered — poll as often as you like\n\nNo API key, no signup, no credit card required. Rate limiting is enforced by IP address (`CF-Connecting-IP` header when behind Cloudflare).",
    "contact": {
      "name": "BulkNumberChecker Support",
      "url": "https://www.bulknumberchecker.com/about",
      "email": "tbsidevs@gmail.com"
    },
    "license": {
      "name": "Free to use",
      "url": "https://www.bulknumberchecker.com/terms"
    },
    "x-logo": {
      "url": "https://www.bulknumberchecker.com/icon.png",
      "altText": "BulkNumberChecker"
    }
  },
  "servers": [
    {
      "url": "https://api.bulknumberchecker.com",
      "description": "Production API (Cloudflare-proxied)"
    }
  ],
  "paths": {
    "/api/v1/check": {
      "post": {
        "operationId": "submitNumbers",
        "summary": "Submit phone numbers for WhatsApp validation",
        "description": "Submit a list of phone numbers to be checked against WhatsApp. Numbers are processed asynchronously. The response includes a `request_id` you can use to poll for results.\n\nNumbers should be in E.164 format (e.g. `+14155551234`) but common variants (spaces, dashes, parentheses) are normalized automatically.\n\n**Rate limit:** 5 requests per IP per day. Each call counts as 1 request regardless of how many numbers are submitted.",
        "x-rate-limit": {
          "requests-per-day": 5,
          "max-numbers-per-request": 10,
          "scope": "ip",
          "reset": "UTC midnight"
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubmitRequest"
              },
              "examples": {
                "single": {
                  "summary": "Single number",
                  "value": {
                    "numbers": ["+14155551234"]
                  }
                },
                "bulk": {
                  "summary": "Multiple numbers with default country",
                  "value": {
                    "numbers": ["+6281234567890", "+14155551234", "+447911123456"],
                    "default_country": "ID"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Job accepted. Use `status_url` or `GET /api/v1/check/{id}` to poll for results.",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": { "type": "integer" },
                "description": "Maximum requests per day for this IP"
              },
              "X-RateLimit-Remaining": {
                "schema": { "type": "integer" },
                "description": "Requests remaining today"
              },
              "X-RateLimit-Reset": {
                "schema": { "type": "integer" },
                "description": "Unix timestamp when the quota resets"
              },
              "Access-Control-Allow-Origin": {
                "schema": { "type": "string", "enum": ["*"] },
                "description": "CORS header — all origins are allowed"
              },
              "Cache-Control": {
                "schema": { "type": "string", "enum": ["no-store"] },
                "description": "Responses are never cached"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubmitResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request body",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "missing_numbers": {
                    "summary": "Missing numbers field",
                    "value": {
                      "error": "missing_numbers",
                      "message": "Request body must include a non-empty `numbers` array."
                    }
                  },
                  "too_many_numbers": {
                    "summary": "Exceeds per-request limit",
                    "value": {
                      "error": "too_many_numbers",
                      "max": 10,
                      "message": "Free tier allows at most 10 numbers per request."
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Daily rate limit exceeded for this IP",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimitError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/check/{id}": {
      "get": {
        "operationId": "getStatus",
        "summary": "Poll validation status",
        "description": "Retrieve the current status and results for a validation job. This endpoint is **unmetered** — it does not consume your daily quota. Poll every 3–5 seconds until `status` is `completed` or `failed`.",
        "x-rate-limit": {
          "metered": false,
          "description": "Polling endpoints are unmetered. No quota consumed."
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The `request_id` returned by `POST /api/v1/check`",
            "schema": {
              "type": "string",
              "example": "req_abc123xyz"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Job status and results (results are null until status is `completed`)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatusResponse"
                }
              }
            }
          },
          "404": {
            "description": "No job found with that ID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "error": "not_found",
                  "message": "No validation job with that ID."
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/check/{id}/csv": {
      "get": {
        "operationId": "downloadCsv",
        "summary": "Download results as CSV",
        "description": "Download the completed validation results as a CSV file. Returns `404` with `{ error: \"not_ready\" }` if the job is not yet completed. This endpoint is **unmetered**.\n\nCSV columns: `Input`, `Phone Number`, `Country Code`, `Country`, `WhatsApp Status`.",
        "x-rate-limit": {
          "metered": false,
          "description": "Download endpoints are unmetered. No quota consumed."
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The `request_id` returned by `POST /api/v1/check`",
            "schema": {
              "type": "string",
              "example": "req_abc123xyz"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "CSV file download",
            "headers": {
              "Content-Disposition": {
                "schema": { "type": "string" },
                "description": "Attachment filename: `wachecker-{id}.csv`",
                "example": "attachment; filename=\"wachecker-req_abc123xyz.csv\""
              }
            },
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "CSV data with header row",
                  "example": "Input,Phone Number,Country Code,Country,WhatsApp Status\n+6281234567890,+6281234567890,ID,Indonesia,valid\n+14155552671,+14155552671,US,United States,invalid"
                }
              }
            }
          },
          "404": {
            "description": "Job not found or not yet completed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "not_ready": {
                    "summary": "Job still in progress",
                    "value": {
                      "error": "not_ready",
                      "message": "Validation is not yet complete. Poll GET /api/v1/check/{id} until status is completed."
                    }
                  },
                  "not_found": {
                    "summary": "Unknown ID",
                    "value": {
                      "error": "not_found",
                      "message": "No validation job with that ID."
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "SubmitRequest": {
        "type": "object",
        "required": ["numbers"],
        "properties": {
          "numbers": {
            "type": "array",
            "items": { "type": "string" },
            "minItems": 1,
            "maxItems": 10,
            "description": "Array of phone numbers to check. E.164 format preferred (e.g. `+14155551234`). Common formatting variants are normalized automatically.",
            "example": ["+6281234567890", "+14155552671"]
          },
          "default_country": {
            "type": "string",
            "description": "ISO 3166-1 alpha-2 country code used to interpret numbers that lack a country prefix (e.g. `ID` for Indonesia, `US` for United States). Optional.",
            "example": "ID"
          }
        }
      },
      "SubmitResponse": {
        "type": "object",
        "required": ["request_id", "total", "cached", "pending", "status", "status_url", "rate_limit"],
        "properties": {
          "request_id": {
            "type": "string",
            "description": "Unique identifier for this validation job. Use it to poll status or download CSV.",
            "example": "req_abc123xyz"
          },
          "total": {
            "type": "integer",
            "description": "Total number of phone numbers in this job.",
            "example": 3
          },
          "cached": {
            "type": "integer",
            "description": "Number of results served from cache (already checked previously).",
            "example": 1
          },
          "pending": {
            "type": "integer",
            "description": "Number of numbers that need live WhatsApp validation.",
            "example": 2
          },
          "status": {
            "$ref": "#/components/schemas/ValidationStatus"
          },
          "status_url": {
            "type": "string",
            "format": "uri",
            "description": "Absolute URL to poll for job status.",
            "example": "https://api.bulknumberchecker.com/api/v1/check/req_abc123xyz"
          },
          "rate_limit": {
            "$ref": "#/components/schemas/RateLimitInfo"
          }
        }
      },
      "StatusResponse": {
        "type": "object",
        "required": ["request_id", "status", "progress"],
        "properties": {
          "request_id": {
            "type": "string",
            "description": "Unique identifier for this validation job.",
            "example": "req_abc123xyz"
          },
          "status": {
            "$ref": "#/components/schemas/ValidationStatus"
          },
          "progress": {
            "type": "object",
            "required": ["total", "completed", "cached"],
            "properties": {
              "total": {
                "type": "integer",
                "description": "Total numbers in this job.",
                "example": 3
              },
              "completed": {
                "type": "integer",
                "description": "Numbers checked so far.",
                "example": 2
              },
              "cached": {
                "type": "integer",
                "description": "Numbers served from cache.",
                "example": 1
              }
            }
          },
          "results": {
            "type": "array",
            "nullable": true,
            "description": "Array of per-number results. `null` until status is `completed`.",
            "items": {
              "$ref": "#/components/schemas/PhoneResult"
            }
          },
          "eta_seconds": {
            "type": "integer",
            "nullable": true,
            "description": "Estimated seconds until completion, based on current throughput. `null` if already completed or unknown.",
            "example": 30
          }
        }
      },
      "PhoneResult": {
        "type": "object",
        "required": ["input", "phoneNumber", "countryCode", "countryName", "whatsappStatus"],
        "properties": {
          "input": {
            "type": "string",
            "description": "The original number as submitted by the caller.",
            "example": "+6281234567890"
          },
          "phoneNumber": {
            "type": "string",
            "description": "Normalized E.164 phone number.",
            "example": "+6281234567890"
          },
          "countryCode": {
            "type": "string",
            "description": "ISO 3166-1 alpha-2 country code detected from the number prefix.",
            "example": "ID"
          },
          "countryName": {
            "type": "string",
            "description": "Human-readable country name.",
            "example": "Indonesia"
          },
          "whatsappStatus": {
            "type": "string",
            "enum": ["valid", "invalid", "unknown"],
            "description": "`valid` — number is registered on WhatsApp. `invalid` — not registered. `unknown` — check could not be completed (retry).",
            "example": "valid"
          }
        }
      },
      "ValidationStatus": {
        "type": "string",
        "enum": ["pending", "processing", "completed", "failed"],
        "description": "`pending` — queued, not yet started. `processing` — worker is checking numbers. `completed` — all numbers checked, CSV available. `failed` — unrecoverable error.",
        "example": "processing"
      },
      "RateLimitInfo": {
        "type": "object",
        "required": ["limit", "remaining", "reset_at"],
        "properties": {
          "limit": {
            "type": "integer",
            "description": "Maximum requests allowed per day for this IP.",
            "example": 5
          },
          "remaining": {
            "type": "integer",
            "description": "Requests remaining today.",
            "example": 4
          },
          "reset_at": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 timestamp when the daily quota resets (UTC midnight).",
            "example": "2026-04-16T00:00:00.000Z"
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "string",
            "description": "Machine-readable error code.",
            "example": "too_many_numbers"
          },
          "message": {
            "type": "string",
            "description": "Human-readable description of the error.",
            "example": "Free tier allows at most 10 numbers per request."
          },
          "max": {
            "type": "integer",
            "description": "Present on `too_many_numbers` errors — the maximum allowed count.",
            "example": 10
          }
        }
      },
      "RateLimitError": {
        "type": "object",
        "required": ["error", "limit", "remaining", "reset_at"],
        "properties": {
          "error": {
            "type": "string",
            "enum": ["rate_limit_exceeded"],
            "example": "rate_limit_exceeded"
          },
          "limit": {
            "type": "integer",
            "example": 5
          },
          "remaining": {
            "type": "integer",
            "example": 0
          },
          "reset_at": {
            "type": "string",
            "format": "date-time",
            "description": "ISO 8601 timestamp when the quota resets.",
            "example": "2026-04-16T00:00:00.000Z"
          }
        }
      }
    }
  }
}
