{
  "openapi": "3.1.0",
  "info": {
    "title": "B2B Database API",
    "version": "1.0.0",
    "summary": "Self-serve developer API for verified B2B contact data.",
    "description": "Faceted search is free; you spend a credit only when you reveal a real contact, and invalid-verdict emails are auto-refunded. The same credit balance, suppression list, and verification guarantees as the web app apply. Authenticate with an instant API key (`Authorization: Bearer b2db_live_…`).",
    "termsOfService": "https://b2bdatabase.net/terms",
    "contact": { "name": "B2B Database Support", "url": "https://b2bdatabase.net/api-docs" }
  },
  "servers": [{ "url": "https://b2bdatabase.net", "description": "Production" }],
  "security": [{ "bearerAuth": [] }],
  "tags": [
    { "name": "Search", "description": "Free faceted search (masked emails)." },
    { "name": "Reveal", "description": "Credit-metered reveal & enrichment." },
    { "name": "Account", "description": "Usage, credits, suppression." }
  ],
  "paths": {
    "/v1/search": {
      "get": {
        "tags": ["Search"],
        "operationId": "searchLeads",
        "summary": "Faceted contact search (0 credits).",
        "description": "Returns masked contacts. Each lead carries its email_status verdict and verified_at freshness. Suppressed records are never returned.",
        "parameters": [
          { "name": "query", "in": "query", "schema": { "type": "string" }, "description": "Free-text over name / company / title / keywords." },
          { "name": "company_name", "in": "query", "schema": { "type": "string" } },
          { "name": "industry", "in": "query", "schema": { "type": "array", "items": { "type": "string" } }, "style": "form", "explode": true },
          { "name": "country", "in": "query", "schema": { "type": "array", "items": { "type": "string" } }, "style": "form", "explode": true },
          { "name": "state", "in": "query", "schema": { "type": "array", "items": { "type": "string" } }, "style": "form", "explode": true },
          { "name": "city", "in": "query", "schema": { "type": "array", "items": { "type": "string" } }, "style": "form", "explode": true },
          { "name": "job_title", "in": "query", "schema": { "type": "array", "items": { "type": "string" } }, "style": "form", "explode": true },
          { "name": "seniority_level", "in": "query", "schema": { "type": "array", "items": { "type": "string" } }, "style": "form", "explode": true },
          { "name": "department", "in": "query", "schema": { "type": "array", "items": { "type": "string" } }, "style": "form", "explode": true },
          { "name": "company_size", "in": "query", "schema": { "type": "array", "items": { "type": "string" } }, "style": "form", "explode": true },
          { "name": "email_status", "in": "query", "schema": { "type": "array", "items": { "$ref": "#/components/schemas/EmailVerdict" } }, "style": "form", "explode": true },
          { "name": "quality_tier", "in": "query", "schema": { "type": "array", "items": { "type": "string", "enum": ["A", "B", "C", "D"] } }, "style": "form", "explode": true },
          { "name": "page", "in": "query", "schema": { "type": "integer", "minimum": 1, "default": 1 } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 25 } },
          { "name": "sort", "in": "query", "schema": { "type": "string", "enum": ["recency", "score"] } }
        ],
        "responses": {
          "200": {
            "description": "Search results (masked).",
            "headers": { "$ref": "#/components/headers/RateLimit" },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["leads", "total", "estimated", "page", "limit", "request_id"],
                  "properties": {
                    "leads": { "type": "array", "items": { "$ref": "#/components/schemas/Lead" } },
                    "total": { "type": "integer" },
                    "estimated": { "type": "boolean", "description": "True when total is reported as an estimate (\"10,000+\")." },
                    "page": { "type": "integer" },
                    "limit": { "type": "integer" },
                    "request_id": { "type": "string" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/v1/reveal": {
      "post": {
        "tags": ["Reveal"],
        "operationId": "revealContacts",
        "summary": "Reveal full contacts by id (1 credit / new contact).",
        "description": "Idempotent: re-revealing an owned contact is free. Invalid-verdict emails are auto-refunded. Suppressed / missing ids are never charged or returned.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["lead_ids"],
                "properties": {
                  "lead_ids": { "type": "array", "items": { "type": "string" }, "minItems": 1, "maxItems": 1000, "description": "Lead ids from /v1/search." }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Revealed contacts + credit accounting.",
            "headers": { "$ref": "#/components/headers/RateLimit" },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RevealResult" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "$ref": "#/components/responses/InsufficientCredits" },
          "404": { "$ref": "#/components/responses/NoRevealableLeads" },
          "413": { "$ref": "#/components/responses/PayloadTooLarge" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/v1/lookup": {
      "get": {
        "tags": ["Reveal"],
        "operationId": "lookupByEmail",
        "summary": "Reverse-resolve one email to a full record (1 credit on hit, 0 on miss).",
        "parameters": [
          { "name": "email", "in": "query", "required": true, "schema": { "type": "string", "format": "email" } }
        ],
        "responses": {
          "200": {
            "description": "Lookup result.",
            "headers": { "$ref": "#/components/headers/RateLimit" },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["found", "lead", "charged", "refunded", "free_reowned", "balance", "request_id"],
                  "properties": {
                    "found": { "type": "boolean" },
                    "lead": { "oneOf": [{ "$ref": "#/components/schemas/Lead" }, { "type": "null" }] },
                    "charged": { "type": "integer" },
                    "refunded": { "type": "integer" },
                    "free_reowned": { "type": "boolean" },
                    "balance": { "type": "integer" },
                    "request_id": { "type": "string" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/v1/enrich": {
      "post": {
        "tags": ["Reveal"],
        "operationId": "enrichRows",
        "summary": "Bulk-enrich up to 1000 identity rows (1 credit / match).",
        "description": "Each row matches by email (exact) or name + company/company_domain. Unmatched rows and already-owned contacts cost 0; invalids refund. Stops early with truncated:true on insufficient credits.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["rows"],
                "properties": {
                  "rows": {
                    "type": "array",
                    "maxItems": 1000,
                    "items": { "$ref": "#/components/schemas/EnrichInput" }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Per-row enrichment results.",
            "headers": { "$ref": "#/components/headers/RateLimit" },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["results", "matched", "charged", "refunded", "balance", "total_rows", "truncated", "request_id"],
                  "properties": {
                    "results": { "type": "array", "items": { "$ref": "#/components/schemas/EnrichResult" } },
                    "matched": { "type": "integer" },
                    "charged": { "type": "integer" },
                    "refunded": { "type": "integer" },
                    "balance": { "type": "integer" },
                    "total_rows": { "type": "integer" },
                    "truncated": { "type": "boolean", "description": "True if the batch stopped early on insufficient credits." },
                    "request_id": { "type": "string" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "413": { "$ref": "#/components/responses/PayloadTooLarge" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/v1/usage": {
      "get": {
        "tags": ["Account"],
        "operationId": "getUsage",
        "summary": "Credit balance + this key's usage (0 credits).",
        "parameters": [
          { "name": "days", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 365, "default": 30 } }
        ],
        "responses": {
          "200": {
            "description": "Balance + usage rollups.",
            "headers": { "$ref": "#/components/headers/RateLimit" },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["balance", "usage", "rate_limit", "request_id"],
                  "properties": {
                    "balance": { "type": "integer" },
                    "usage": {
                      "type": "object",
                      "properties": {
                        "total": { "type": "integer" },
                        "by_endpoint": { "type": "array", "items": { "type": "object", "properties": { "endpoint": { "type": "string" }, "count": { "type": "integer" } } } },
                        "by_day": { "type": "array", "items": { "type": "object", "properties": { "day": { "type": "string" }, "count": { "type": "integer" } } } },
                        "since": { "type": "string" }
                      }
                    },
                    "rate_limit": { "type": "object", "properties": { "limit": { "type": "integer" }, "remaining": { "type": "integer" }, "reset": { "type": "integer" } } },
                    "request_id": { "type": "string" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/v1/suppression": {
      "post": {
        "tags": ["Account"],
        "operationId": "pushSuppression",
        "summary": "Programmatic opt-out / Do-Not-Sell push (0 credits).",
        "description": "Adds emails to the permanent suppression list. Matching records are then never served anywhere. Idempotent.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": { "type": "string", "format": "email" },
                  "emails": { "type": "array", "items": { "type": "string", "format": "email" }, "maxItems": 1000 },
                  "reason": { "type": "string", "enum": ["partner", "dsr", "drop", "self_service"], "default": "partner" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Suppression accepted.",
            "headers": { "$ref": "#/components/headers/RateLimit" },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["suppressed", "reason", "request_id"],
                  "properties": {
                    "suppressed": { "type": "integer" },
                    "reason": { "type": "string" },
                    "request_id": { "type": "string" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "b2db_live_…",
        "description": "An API key minted from your dashboard. Sent as `Authorization: Bearer b2db_live_…`."
      }
    },
    "headers": {
      "RateLimit": {
        "X-RateLimit-Limit": { "schema": { "type": "integer" }, "description": "Requests allowed per window." },
        "X-RateLimit-Remaining": { "schema": { "type": "integer" }, "description": "Requests remaining in the window." },
        "X-RateLimit-Reset": { "schema": { "type": "integer" }, "description": "Unix epoch seconds when the window resets." }
      }
    },
    "schemas": {
      "EmailVerdict": {
        "type": "string",
        "enum": ["valid", "catch_all", "risky", "invalid", "unknown"],
        "description": "Layered verification verdict. Catch-alls are never labelled \"verified\"."
      },
      "Lead": {
        "type": "object",
        "description": "A contact record. In /v1/search responses, email and personal_email are masked teasers; in reveal/lookup/enrich responses they are unmasked.",
        "properties": {
          "id": { "type": "string" },
          "first_name": { "type": ["string", "null"] },
          "last_name": { "type": ["string", "null"] },
          "full_name": { "type": ["string", "null"] },
          "email": { "type": ["string", "null"] },
          "email_status": { "$ref": "#/components/schemas/EmailVerdict" },
          "email_type": { "type": ["string", "null"], "enum": ["work", "personal", "unknown", null] },
          "personal_email": { "type": ["string", "null"] },
          "verified_at": { "type": ["string", "null"], "description": "ISO date the verdict was last established." },
          "linkedin_url": { "type": ["string", "null"] },
          "job_title": { "type": ["string", "null"] },
          "department": { "type": ["string", "null"] },
          "seniority_level": { "type": ["string", "null"] },
          "company_name": { "type": ["string", "null"] },
          "company_website": { "type": ["string", "null"] },
          "industry": { "type": ["string", "null"] },
          "company_size": { "type": ["string", "null"] },
          "city": { "type": ["string", "null"] },
          "state": { "type": ["string", "null"] },
          "country": { "type": ["string", "null"] },
          "phone": { "type": ["string", "null"] },
          "company_phone": { "type": ["string", "null"] },
          "keywords": { "type": ["string", "null"] },
          "employees": { "type": ["string", "null"] },
          "annual_revenue": { "type": ["string", "null"] },
          "founded_year": { "type": ["string", "null"] },
          "technologies": { "type": ["string", "null"] },
          "quality_tier": { "type": "string", "enum": ["A", "B", "C", "D"] },
          "source": { "type": ["string", "null"] },
          "source_batch_id": { "type": ["string", "null"] }
        },
        "required": ["id", "email_status", "quality_tier"]
      },
      "RevealResult": {
        "type": "object",
        "required": ["leads", "charged", "refunded", "free_reowned", "balance", "verdict_mix", "request_id"],
        "properties": {
          "leads": { "type": "array", "items": { "$ref": "#/components/schemas/Lead" } },
          "charged": { "type": "integer", "description": "Credits spent (new contacts minus refunded invalids)." },
          "refunded": { "type": "integer", "description": "Credits refunded for invalid-verdict emails." },
          "free_reowned": { "type": "integer", "description": "Already-owned contacts re-revealed for free." },
          "balance": { "type": "integer", "description": "Credit balance after this request." },
          "verdict_mix": {
            "type": "object",
            "properties": {
              "valid": { "type": "integer" },
              "catch_all": { "type": "integer" },
              "risky": { "type": "integer" },
              "invalid": { "type": "integer" },
              "unknown": { "type": "integer" }
            }
          },
          "request_id": { "type": "string" }
        }
      },
      "EnrichInput": {
        "type": "object",
        "description": "One identity to resolve. Provide email for an exact match, or name + company/company_domain.",
        "properties": {
          "ref": { "description": "Caller-supplied id echoed back on the result row.", "type": ["string", "number"] },
          "email": { "type": "string", "format": "email" },
          "full_name": { "type": "string" },
          "first_name": { "type": "string" },
          "last_name": { "type": "string" },
          "company_name": { "type": "string" },
          "company_domain": { "type": "string" },
          "job_title": { "type": "string" }
        }
      },
      "EnrichResult": {
        "type": "object",
        "required": ["ref", "matched", "lead", "charged", "refunded"],
        "properties": {
          "ref": { "type": ["string", "number", "null"] },
          "matched": { "type": "boolean" },
          "lead": { "oneOf": [{ "$ref": "#/components/schemas/Lead" }, { "type": "null" }] },
          "charged": { "type": "integer" },
          "refunded": { "type": "integer" }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "object",
            "required": ["code", "message", "docs_url", "request_id"],
            "properties": {
              "code": {
                "type": "string",
                "enum": ["unauthorized", "invalid_key", "forbidden", "invalid_request", "invalid_json", "not_found", "no_revealable_leads", "insufficient_credits", "rate_limited", "payload_too_large", "method_not_allowed", "internal_error"]
              },
              "message": { "type": "string" },
              "docs_url": { "type": "string" },
              "request_id": { "type": "string" }
            }
          }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing, malformed, revoked, or invalid API key.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "BadRequest": {
        "description": "Malformed request parameters or body.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "InsufficientCredits": {
        "description": "Not enough credits. The error context includes needed + available.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "NoRevealableLeads": {
        "description": "No live, non-suppressed records matched the supplied ids.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "PayloadTooLarge": {
        "description": "Too many ids / rows in the request.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "RateLimited": {
        "description": "Rate limit exceeded. See Retry-After + X-RateLimit-* headers.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    }
  }
}
