{
  "openapi": "3.1.0",
  "info": {
    "title": "NumberBroom phone verification",
    "version": "1.0",
    "description": "Pay-as-you-go lookups for US phone numbers: line type, carrier, activity score and known litigator status. Each lookup is charged against pre-paid credits; an unparseable number or a number outside the US is free, and a failed lookup is refunded. /v1/verify does not check any Do Not Call registry; a list sent to /v1/lists gets the federal Do Not Call Registry check like an upload from the same account (only under the account's own verified SAN, on lists of up to 1,000 numbers). How to get an API key: https://numberbroom.com/auth.md",
    "contact": { "url": "https://numberbroom.com/contact" }
  },
  "externalDocs": { "url": "https://numberbroom.com/developers" },
  "servers": [{ "url": "https://numberbroom.com/api" }],
  "security": [{ "apiKey": [] }],
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "A NumberBroom API key (nb_live_...). A person creates it in Settings at https://numberbroom.com/settings; see https://numberbroom.com/auth.md."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": { "type": "string" },
          "message": { "type": "string" }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "No API key, or a key that is invalid or revoked.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    }
  },
  "paths": {
    "/v1/verify": {
      "post": {
        "operationId": "verifyPhoneNumber",
        "summary": "Verify one US phone number. Returns line type, carrier, activity score and known litigator status. Costs $0.20; an unparseable number or one outside the US is free.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["phone"],
                "properties": {
                  "phone": { "type": "string", "description": "The number in any common US format." }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Verification result. valid is false when the input is not a US phone number, or when its carrier data is withheld (reason suppressed).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "phone": { "type": "string", "description": "The input as sent" },
                    "e164": { "type": ["string", "null"], "description": "null when the input does not parse" },
                    "valid": { "type": "boolean" },
                    "reason": { "type": "string", "enum": ["unparseable", "not_nanp", "outside_us", "suppressed"], "description": "Present only when valid is false: why the number was not checked. suppressed is permanent: asking again returns the same" },
                    "message": { "type": "string", "description": "Present only when valid is false" },
                    "lineType": { "type": "string" },
                    "carrier": { "type": ["string", "null"] },
                    "isLitigator": { "type": ["boolean", "null"], "description": "true when court filings tie the number to a TCPA/FDCPA/FCRA/CFPB suit; the answer does not say which law, and no database catches every litigator. null when no litigator check ran: the carrier lookup reported the number as not valid (outcome invalid)" },
                    "litigator": { "type": "string", "enum": ["yes", "no", "not_checked"], "description": "isLitigator as a word, for no-code tools that store and compare text (GoHighLevel, Google Sheets): not_checked when no litigator check ran" },
                    "activityScore": { "type": ["number", "null"], "description": "0 to 100; null when the carrier data has no signal" },
                    "isLikelyDisconnected": { "type": ["boolean", "null"], "description": "true at an activity score of 30 or below; null when there is no score" },
                    "outcome": { "type": "string" },
                    "keep": { "type": "boolean" },
                    "dncEvaluated": { "type": "boolean" },
                    "charged": { "type": "number" },
                    "creditsRemaining": { "type": "number" }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No phone field, or one too long to be a number. Not charged.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": {
            "description": "Not enough API credits. A person tops up in Settings. Not charged.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "429": {
            "description": "A daily limit was reached (the key's, or the account's). Resets at midnight UTC. Not charged.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "502": {
            "description": "The carrier lookup returned no usable result. Not charged; safe to retry later.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "503": {
            "description": "Lookups are paused. Not charged; try again shortly.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/v1/credits": {
      "get": {
        "operationId": "getCreditBalance",
        "summary": "Remaining pre-paid balance and how many lookups it covers. Free to call.",
        "responses": {
          "200": {
            "description": "Balance",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "credits": { "type": "number" },
                    "ratePerLookup": { "type": "number" },
                    "lookupsRemaining": { "type": "integer" },
                    "dailyLimit": { "type": "integer", "description": "Lookups a day this key may make when called the way this request was" },
                    "accountDailyLimit": { "type": "integer", "description": "Lookups a day across every key on the account" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/v1/credits/checkout": {
      "post": {
        "operationId": "createCreditCheckout",
        "summary": "A Stripe Checkout link that adds credit to this key's account when a person pays it. Free to call.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["amount"],
                "properties": {
                  "amount": { "type": "number", "description": "Dollars of credit, 10 to 5000." }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Open url to pay. Credit lands when the payment clears.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": { "type": "string" },
                    "sessionId": { "type": "string" }
                  }
                }
              }
            }
          },
          "400": {
            "description": "amount is outside 10 to 5000.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": {
            "description": "The account has been deleted.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "429": {
            "description": "Too many checkout links today.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/v1/lists": {
      "post": {
        "operationId": "createListCheckout",
        "summary": "Send a whole list for the paid list scrub. Returns the price at list rates and a Stripe Checkout link for a person to pay; nothing is taken from API credit.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["numbers"],
                "properties": {
                  "numbers": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 10000,
                    "items": { "type": "string", "maxLength": 40 },
                    "description": "Phone numbers, digits and phone punctuation only."
                  },
                  "scrubMode": { "type": "string", "enum": ["full", "litigator_only"], "description": "full removes litigators, disconnected lines and anything not a live mobile; litigator_only removes known litigators and numbers that are not valid, and keeps landlines, VoIP and disconnected numbers." }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The price and the link to pay it. The scrub runs once payment clears.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobId": { "type": "string" },
                    "url": { "type": "string" },
                    "rowCount": { "type": "integer" },
                    "chargedNumbers": { "type": "integer" },
                    "outsideUsRows": { "type": "integer", "description": "Numbers outside the US (Canada, Puerto Rico, the Caribbean, overseas): not priced or scrubbed, and returned in their own file once the scrub completes" },
                    "amount": { "type": "number" },
                    "scrubMode": { "type": "string" },
                    "message": { "type": "string" }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A number is not phone characters, the list is empty or over 10,000, or nothing in it can be scrubbed.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": {
            "description": "The account has been deleted.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "429": {
            "description": "Too many lists today.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/v1/lists/{jobId}": {
      "get": {
        "operationId": "getListStatus",
        "summary": "Status and counts for one of this account's list scrubs, with a download link that lasts an hour once it is done.",
        "parameters": [{ "name": "jobId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": {
            "description": "Counts appear once status is complete.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobId": { "type": "string" },
                    "status": { "type": "string" },
                    "paid": { "type": "boolean" },
                    "scrubMode": { "type": "string" },
                    "totalNumbers": { "type": ["integer", "null"] },
                    "amount": { "type": ["number", "null"] },
                    "dashboardUrl": { "type": "string" },
                    "cleanCount": { "type": "integer" },
                    "litigatorCount": { "type": "integer" },
                    "voipLandlineCount": { "type": "integer" },
                    "disconnectedCount": { "type": "integer" },
                    "invalidCount": { "type": "integer", "description": "Numbers that are not valid phone numbers (removed in either scrub mode); absent on lists scrubbed before 2026-10-05" },
                    "errorCount": { "type": "integer" },
                    "ownDncCount": { "type": "integer" },
                    "outsideUsRows": { "type": "integer", "description": "Numbers outside the US, left out of the scrub and not charged; absent on lists scrubbed before 2026-10-05" },
                    "downloadUrl": { "type": "string" },
                    "outsideUsDownloadUrl": { "type": "string", "description": "Those numbers' rows, as sent, in a file of their own; present only when the list had any. Lasts as long as downloadUrl" },
                    "removedDownloadUrl": { "type": "string", "description": "The removed rows, as sent, with line_type, carrier, activity_score and removed_because, known litigators included; never a Do Not Call Registry match. Present only when rows were removed. Lasts as long as downloadUrl" },
                    "litigatorDownloadUrl": { "type": "string", "description": "The known litigator rows alone, as sent, with line_type, carrier and activity_score; present only when the list had any, and absent on lists scrubbed before 2026-10-14. Lasts as long as downloadUrl" },
                    "downloadExpiresAt": { "type": "string" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": {
            "description": "The account has been deleted.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "404": {
            "description": "No list with that id on this account.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    }
  }
}
