# Account Reporter v2 Source: https://docs.waterfall.io/v1/account-reporter-v2 api_specs.yaml GET /v2/account Get key-level and account-level usage metrics for a calendar month or a custom date range. This call is free — it does not consume credits or count against your rate limit. ## What this endpoint does Returns usage counters and remaining balance for both the authenticated API key (`key_usage`) and the entire account (`account_usage`). The two objects are identical if you only have one key. If you have multiple keys, `account_usage` reflects the sum across all keys. ## Query parameters Usage can be scoped one of two ways. The two modes are mutually exclusive. **By month** — pass `month` in `YYYY-MM` format. If omitted, returns the current month. ``` GET /v2/account?month=2024-10 ``` **By custom date range** — pass `start_date` and `end_date` together in `YYYY-MM-DD` format (UTC). `start_date` is inclusive, `end_date` is exclusive, and the range cannot exceed 12 months. ``` GET /v2/account?start_date=2025-01-01&end_date=2025-01-15 ``` In both modes, the window's start cannot be more than 12 months in the past, and the window cannot be entirely in the future. Combining `month` with `start_date`/`end_date`, or passing only one of `start_date`/`end_date`, returns a `400`. ## Usage fields Both `key_usage` and `account_usage` return the same set of counters: | Field | Description | | ----------------------------------- | ---------------------------------------------------------------------- | | `prospector_requests` | Number of Prospector Launcher jobs submitted | | `prospector_persons` | Number of contacts returned by Prospector | | `prospector_persons_phones` | Phone numbers returned via Prospector phone enrichment | | `enrichment_contact_requests` | Contact Enrichment Launcher jobs submitted | | `enrichment_contact_persons` | Contacts successfully enriched | | `enrichment_contact_persons_phones` | Phone numbers returned via Contact Enrichment | | `enrichment_phone_requests` | Phone Enrichment Launcher jobs submitted | | `enrichment_phone_phones` | Phone numbers successfully returned | | `enrichment_company_requests` | Company Enrichment Launcher jobs submitted | | `enrichment_company_companies` | Companies successfully enriched | | `search_contact_requests` | Search Contact Launcher jobs submitted | | `search_contact_found` | Contacts returned by Search Contact | | `verify_email_requests` | Email Verifier calls made | | `verify_email_verified` | Emails that returned a definitive status (`valid`, `invalid`, `risky`) | | `balance_remaining_usd` | Remaining account balance in USD | `balance_remaining_usd` is always the current balance, and `price` (master keys only) is always the current unit price. Neither is a historical value for the requested month or date range. # API Keys - Create Source: https://docs.waterfall.io/v1/api-keys-create api_specs.yaml POST /v1/api-keys Create a new sub-key for your account. Requires your **master API key** in the `x-api-key` header. ## What this endpoint does Creates a new sub-key under your account. Use sub-keys to isolate usage by team, environment, or integration — each key has its own rate limit and can be activated or deactivated independently. ## Rate limit constraint The `per_interval` of any sub-key cannot exceed the `per_interval` of your master key. If omitted, it defaults to `30` requests per 60 seconds. If your account does not have a master key, this endpoint returns `404`. Contact your account manager to enable key management. # API Keys - List Source: https://docs.waterfall.io/v1/api-keys-list api_specs.yaml GET /v1/api-keys Get a list of all API keys under your account. Requires your **master API key** in the `x-api-key` header. ## What this endpoint does Returns all API keys on your account — including the master key and all sub-keys — with their current status, notes, and rate-limit settings. Use this to audit which keys are active, check their rate limits, or find the UUID of a key you want to modify. # API Keys - Modify Source: https://docs.waterfall.io/v1/api-keys-modify api_specs.yaml PUT /v1/api-keys Update notes, active status, or rate limit for an existing sub-key. Requires your **master API key** in the `x-api-key` header. ## What can be changed | Field | Editable | Notes | | -------------- | -------- | --------------------------------------------------------------------------------- | | `notes` | Yes | Update the label for any sub-key | | `active` | Yes | Set to `false` to deactivate, `true` to reactivate | | `per_interval` | Yes | Cannot exceed the master key's `per_interval`. If omitted, current value is kept. | | `api_key` | No | Read-only identifier | | `master` | No | Cannot promote or demote a key | The master key cannot be modified with this endpoint. Attempts to edit it will be rejected. # API Keys Management Source: https://docs.waterfall.io/v1/api-keys-overview Manage your Waterfall API keys programmatically using a master key. API key management is an **enterprise feature**. Contact your account manager to get access. Once enabled, you'll receive a master API key that can create, list, and modify all other keys on your account. ## How it works You use a single **master key** to manage all sub-keys. Sub-keys are the keys your teams, workflows, or integrations use for API calls. The master key itself cannot be modified or deleted. ## Key attributes | Field | Type | Description | | ------------------ | ------------- | -------------------------------------------------------------------------- | | `api_key` | string (UUID) | Unique identifier for the key | | `notes` | string | Label or description — useful for identifying which team or system uses it | | `master` | boolean | `true` for the master key, `false` for all sub-keys | | `active` | boolean | `true` if the key can make API calls, `false` if deactivated | | `per_interval` | integer | Maximum requests allowed per interval | | `interval_seconds` | integer | Interval duration in seconds — always `60` | ## Available operations | Operation | Endpoint | | ----------------------------------- | ------------------- | | [List keys](/v1/api-keys-list) | `GET /v1/api-keys` | | [Create a key](/v1/api-keys-create) | `POST /v1/api-keys` | | [Modify a key](/v1/api-keys-modify) | `PUT /v1/api-keys` | All three endpoints require your **master API key** in the `x-api-key` header. Requests authenticated with a sub-key will be rejected. # Authentication Source: https://docs.waterfall.io/v1/authentication How to authenticate requests to Waterfall API. All requests are authenticated using `x-api-key`. ```bash theme={null} x-api-key: ``` Base API URL is `https://api.waterfall.io/v1/` for v1 endpoints. Account reporter v2 uses `/v2/account`. ## Best practices * Store keys in environment variables. * Rotate keys periodically. * Use dedicated keys per team/workflow when possible. # Get API & Sales Support Source: https://docs.waterfall.io/v1/book-demo Book a call with the Waterfall team. Talk to us about API access, pricing, or integration support. # Company Enrichment Finder Source: https://docs.waterfall.io/v1/company-enrichment-finder api_specs.yaml GET /v1/enrichment/company Retrieve company enrichment job state and output by job_id. Use the `job_id` returned by [Company Enrichment Launcher](/v1/company-enrichment-launcher) to fetch state and results. ## Polling vs webhook If you passed `webhook_url` when launching the job, you don't need to call this endpoint at all — Waterfall will automatically deliver the result to your URL as soon as the job is ready. If you didn't use a webhook, poll this endpoint until `status` is `SUCCEEDED`: ``` GET /v1/enrichment/company?job_id={job_id} ``` Use short exponential backoff — most jobs complete in under 15 seconds. Start with a 2s delay, then 4s, then 8s. ## Job states | Status | Meaning | | ----------- | ----------------------------------------------------- | | `RUNNING` | Job is still processing. Continue polling. | | `SUCCEEDED` | Job is complete. Company data is in `output.company`. | ## What you get The `output.company` object includes: | Field | Description | | ------------------------------ | -------------------------------------- | | `name` | Company name | | `domain` | Primary domain | | `linkedin_id` / `linkedin_url` | LinkedIn slug and full URL | | `size` | Headcount range (e.g. `501-1000`) | | `employees_count` | Estimated headcount | | `industry` | Industry classification | | `type` | Ownership type (e.g. `Privately Held`) | | `founded` | Year founded | | `description` | Company description from LinkedIn | | `address` | HQ address, if available | | `logo_url` | Company logo URL | ## Response shape The `input.task` object echoes back all fields from the original launch request. Fields not provided will be `null`. # Company Enrichment Launcher Source: https://docs.waterfall.io/v1/company-enrichment-launcher api_specs.yaml POST /v1/enrichment/company Enrich a company by domain, LinkedIn, or name. Returns a job_id to retrieve results. **Company Enrichment Overview** **You input:** Domain, LinkedIn URL/slug, or company name **You get:** Headcount, industry, HQ location, company type, description, founded date, and LinkedIn data This is an asynchronous endpoint. Use the returned `job_id` with [Company Enrichment Finder](/v1/company-enrichment-finder) to retrieve results. We recommend passing `webhook_url` for a smoother integration — Waterfall will POST the result directly to your endpoint as soon as the job is ready, so you don't need to poll. ## Input strategies | Priority | Strategy | Required field | Notes | | -------- | ------------ | -------------- | ----------------------------------------------------------------------------------- | | 1 | Domain | `domain` | Most reliable. Accepts full URLs too — Waterfall extracts the domain automatically. | | 2 | LinkedIn | `linkedin` | Use full URL or just the slug (e.g. `acme-corp`). | | 3 | Company name | `name` | Lowest accuracy, highest false-positive risk. Use only as a last resort. | Name-only enrichment can match the wrong company. Always prefer `domain` or `linkedin` when available. ## Business notes Add your internal notes here — pricing tier, credit cost per call, SLA guarantees, data sources used, etc. ## Next step After launching, call [Company Enrichment Finder](/v1/company-enrichment-finder) with the `job_id`: ``` GET /v1/enrichment/company?job_id={job_id} ``` # Company Reveal Finder Source: https://docs.waterfall.io/v1/company-reveal-finder api_specs.yaml GET /v1/company-reveal Retrieve a persisted company reveal job by job_id. Use the `job_id` returned by [Company Reveal Launcher](/v1/company-reveal-launcher) to retrieve a persisted result. This endpoint is in **Beta** — not recommended for critical production workloads. API behavior may change. ## When to use this Company Reveal returns the result synchronously in the Launcher response — you typically don't need to call this endpoint. Use it only if you need to look up a past job by `job_id`. ## Job states | Status | Meaning | | ----------- | ----------------------------------------------- | | `SUCCEEDED` | Job completed. Output is in the `output` field. | | `RUNNING` | Job is still processing. | ## Endpoint details | Metric | Value | | -------------- | -------------------------------------------------------- | | Flow | Sync launcher with persisted job | | Required input | `job_id` query parameter | | Typical usage | Lookup only — result is already in the Launcher response | # Company Reveal Launcher Source: https://docs.waterfall.io/v1/company-reveal-launcher api_specs.yaml POST /v1/company-reveal Reveal company information from an IPv4 address. **Company Reveal Overview** **You input:** An IPv4 address **You get:** Company identity (domain, name, LinkedIn, industry, headcount) plus IP geolocation (city, state, country) and a confidence score This endpoint is in **Beta** — not recommended for critical production workloads. API behavior may change. ## How it works Company Reveal returns the result immediately in the response body — no polling required. The job is also persisted, so you can retrieve it later using the [Company Reveal Finder](/v1/company-reveal-finder) with the returned `job_id`. ## Confidence score The `confidence_score` field indicates how certain Waterfall is that the IP belongs to the identified company. | Value | Meaning | | -------- | ------------------------------------------------------- | | `high` | Strong signal — IP strongly associated with the company | | `medium` | Moderate signal — likely match but not definitive | | `low` | Weak signal — treat result with caution | ## Example request ```json theme={null} { "ip": "8.8.8.8" } ``` ## Passing custom metadata Use `custom_fields` to attach internal IDs or labels — echoed back in the response unchanged. ```json theme={null} { "ip": "8.8.8.8", "custom_fields": { "session_id": "abc123", "source": "inbound-web" } } ``` ## Next step The result is returned directly in the response. To retrieve a past job, use [Company Reveal Finder](/v1/company-reveal-finder) with the `job_id`. # Company Titles Finder Source: https://docs.waterfall.io/v1/company-titles-finder api_specs.yaml GET /v1/company-titles Re-fetch a previous Company Titles job by job_id. Company Titles returns results synchronously from the launcher. Use this endpoint to **re-fetch or audit** a previous job, not to poll for completion. This endpoint is in **Beta**. Behavior and billing may change. Requires your API key to have Company Titles access enabled. ## When to use this Pass the `job_id` returned by [Company Titles Launcher](/v1/company-titles-launcher) to retrieve the same job state and title list without re-running the query. ## Response fields | Field | Description | | ----------------------- | ----------------------------------------------------------------------------------------------- | | `output.titles` | Array of active job titles at the matched company for this page | | `output.has_more_pages` | `true` if additional pages exist. Re-run the Launcher with a higher `page_number` to fetch them | | `usage` | Per-job cost breakdown, present on completed jobs | ## Paginating through results If `has_more_pages` is `true`, launch a new job with the same input and increment `page_number`: ```json theme={null} { "domain": "stripe.com", "page_number": 2 } ``` Each page returns up to 50,000 titles. The valid range is pages 1 through 100. # Company Titles Launcher Source: https://docs.waterfall.io/v1/company-titles-launcher api_specs.yaml POST /v1/company-titles Get all active job titles at a company by domain or LinkedIn. **Company Titles Overview** **You input:** A company domain or LinkedIn handle **You get:** A full list of active job titles currently held at that company, paginated in sets of up to 50,000 titles per page This endpoint is in **Beta**. Behavior and billing may change. Requires your API key to have Company Titles access enabled. Company Titles returns results **synchronously**. `output.titles` is included directly in the response. There is no separate polling step. ## What it's for Company Titles returns every distinct job title Waterfall has for a given company. Rather than filtering by title upfront using boolean or keyword logic, you get the full list and apply your own selection criteria downstream: manual review, rule-based filtering, or feeding the titles into an LLM to decide which roles are worth targeting. ## Recommended use alongside other endpoints The best way to use Company Titles is as a first step before running a [Search Contact](/v1/search-contact-launcher) or [Prospector](/v1/prospector-launcher) job: 1. Call Company Titles to get the full list of active titles at a company 2. Review the titles (manually, programmatically, or via an LLM) to identify the ones you care about 3. Pass those exact titles as a `title_list` in [Search Contact](/v1/search-contact-launcher) to find only the matching contacts This is particularly powerful with Search Contact's `title_lists` filter, which lets you pass exact title strings rather than relying on keyword or boolean matching. Company Titles gives you the raw material; Search Contact uses it precisely. ## Pagination Each page returns up to 50,000 titles. Use `page_number` to paginate (valid range: 1–100). Check `has_more_pages` in the response to know whether additional pages exist. ```json theme={null} { "domain": "acme.com", "page_number": 2 } ``` ## Example request ```json theme={null} { "domain": "stripe.com" } ``` Or by LinkedIn handle: ```json theme={null} { "company_linkedin": "stripe" } ``` ## Passing custom metadata Use `custom_fields` to attach internal IDs. Echoed back in the response unchanged. ```json theme={null} { "domain": "stripe.com", "custom_fields": { "account_id": "acct_8821" } } ``` ## Next step Results are returned directly in the response. To retrieve a past job, use [Company Titles Finder](/v1/company-titles-finder) with the `job_id`. # Contact Enrichment Finder Source: https://docs.waterfall.io/v1/contact-enrichment-finder api_specs.yaml GET /v1/enrichment/contact Retrieve contact enrichment job status and output by job_id. Use the `job_id` returned by [Contact Enrichment Launcher](/v1/contact-enrichment-launcher) to fetch state and results. ## Polling vs webhook If you passed `webhook_url` when launching the job, you don't need to call this endpoint at all — Waterfall will automatically deliver the result to your URL as soon as the job is ready. If you didn't use a webhook, poll this endpoint until `status` is `SUCCEEDED`: ``` GET /v1/enrichment/contact?job_id={job_id} ``` Use short exponential backoff — most jobs complete in under 10 seconds. Start with a 2s delay, then 4s, then 8s. ## Job states | Status | Meaning | | ----------- | ------------------------------------------------- | | `RUNNING` | Job is still processing. Continue polling. | | `SUCCEEDED` | Job is complete. Output is in the `output` field. | ## Response shape The `input.task` object always echoes back all fields from the original launch request. Fields that were not provided will be `null`. This is useful for correlating the result with your original request without needing to store it separately. ## Endpoint details | Metric | Value | | -------------- | ---------------------------------------------- | | Flow | Async finder endpoint | | Required input | `job_id` query parameter | | Typical usage | Poll until `SUCCEEDED`, or receive via webhook | # Contact Enrichment Launcher Source: https://docs.waterfall.io/v1/contact-enrichment-launcher api_specs.yaml POST /v1/enrichment/contact Enrich a contact by email, LinkedIn, or name + domain. Returns a job_id to retrieve results. **Contact Enrichment Overview** **You input:** Professional/personal email, LinkedIn URL/slug, or name + domain **You get:** Verified email, job title, current company, location, employment history, and phone numbers (optional) This is an asynchronous endpoint. Use the returned `job_id` with [Contact Enrichment Finder](/v1/contact-enrichment-finder) to retrieve results. Jobs typically complete in under 10 seconds. We recommend passing `webhook_url` for a smoother integration — Waterfall will POST the result directly to your endpoint as soon as the job is ready, so you don't need to poll. A synchronous version of this endpoint is available for enterprise customers — faster response times and better suited for real-time use cases. [Contact us](https://www.waterfall.io/book-a-call) to learn more. ## Input strategies Pick the identifier you have. Waterfall applies them in priority order if you send more than one. | Priority | Strategy | Required fields | Best for | | -------- | ------------------- | ------------------------------------- | ---------------------------------------------------------- | | 1 | Email | `email` | Highest accuracy. Use a professional email when available. | | 2 | LinkedIn | `linkedin` | Use the profile slug or full URL (`/in/john-doe`). | | 3 | Full name + domain | `full_name` + `domain` | When you have no email or LinkedIn. | | 4 | Split name + domain | `first_name` + `last_name` + `domain` | Same as above, split across two fields. | Only one strategy is evaluated per request (highest priority wins). If you send `email` + `linkedin`, only the email path runs. ## Business notes Add your internal notes here — pricing tier, credit cost per call, SLA guarantees, data sources used, etc. * **Credit cost**: charged per successful enrichment (contact returned), not per launch * **Phone numbers**: set `include_phones: true` to also run phone enrichment — note this may apply additional charges * **Catch-all emails**: by default Waterfall returns only verified safe-to-send addresses; to include catch-all results set `verified_only: false` in a supported endpoint ## Webhook Pass `webhook_url` to receive the Finder payload automatically when enrichment completes — avoids the need to poll. ```json theme={null} { "email": "jane.doe@acme.com", "webhook_url": "https://your-app.com/webhooks/waterfall" } ``` ## Passing custom metadata Use `custom_fields` to attach internal IDs or source labels. These are echoed back in the Finder response unchanged — useful for correlating enrichment results with your CRM records. ```json theme={null} { "email": "jane.doe@acme.com", "custom_fields": { "crm_id": "001ABC", "source": "outbound-q1" } } ``` ## Next step After launching, call [Contact Enrichment Finder](/v1/contact-enrichment-finder) with the `job_id`: ``` GET /v1/enrichment/contact?job_id={job_id} ``` # Email Verifier Source: https://docs.waterfall.io/v1/email-verifier api_specs.yaml POST /v1/verify/email Verify an email address and get deliverability status in a single synchronous call. **Email Verification Overview** **You input:** Business or personal email address **You get:** Real-time deliverability status — `valid`, `risky`, `invalid`, or `unknown` Unlike other Waterfall endpoints, this is **synchronous** — the result is returned immediately in the response. No `job_id`, no polling. ## Status values | Status | Meaning | What to do | | --------- | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- | | `valid` | Safe to send. Mailbox confirmed. | Send. | | `invalid` | High bounce risk. Mailbox does not exist or is disabled. | Do not send. | | `risky` | Potential bounce risk. Sending to risky addresses may cause bounces. | Only send if your blended inbox-level bounce rate is already below 2%. | | `unknown` | Domain not responding — address looks valid but could not be confirmed. Not charged. | Retry later; you won't be billed for this result. | ## Business notes Add your internal notes here — pricing, use cases, suppression logic, retry strategy for `unknown`, etc. * **Billing**: charged per verification call — `unknown` results are not billed * **Catch-all domains**: `risky` frequently indicates a catch-all domain, meaning the domain accepts all inbound mail without confirming individual mailboxes # Errors Source: https://docs.waterfall.io/v1/errors Common HTTP status codes and troubleshooting guidance. Waterfall uses standard HTTP status codes for success and failure. | Status | Meaning | | ------ | ------------------------------ | | `200` | Success | | `400` | Invalid request payload/inputs | | `401` | Missing API key | | `403` | Invalid API key | | `404` | Resource/job not found | | `429` | Rate limit exceeded | | `500` | Internal server error | ## Error response shape All error responses return a JSON body with the following structure: ```json theme={null} { "status": "error", "message": "Bad request", "error": ["Bad request"], "category": "VALIDATION", "code": "VALIDATION_BAD_REQUEST" } ``` | Field | Description | | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `status` | Always `"error"` | | `message` | Human-readable summary | | `error` | Array of error detail strings | | `category` | Broad error group — e.g. `VALIDATION`, `NOT_FOUND`, `RATE_LIMIT` | | `code` | Specific error type — for the full list refer to the [OpenAPI specification](https://waterfall-io-public.s3.us-east-1.amazonaws.com/api_specs.yaml) | ## Troubleshooting checklist * Validate request body structure. * Confirm API key is present and valid. * Retry safely for `429` and transient `5xx`. # Usage Tracking Source: https://docs.waterfall.io/v1/inline-usage Every Waterfall API response includes an output.usage object for real-time cost tracking and balance visibility. Every response includes an `output.usage` object alongside the enrichment payload. No separate API call needed. Cost and balance data is always present. Usage data is returned inside `output`, at the same level as the enrichment, prospector, search, or verification results. ```json theme={null} { "output": { "person": { ... }, "usage": { "total_usd": 0.1, "phones_usd": 0.1, "persons_usd": 0.0, "phones_count": 1, "companies_usd": 0.0, "persons_count": 0, "companies_count": 0, "balance_remaining_usd": 717.486 } } } ``` ## Fields | Field | Description | | ---------------------------------------------------- | --------------------------------------------------------------- | | `total_usd` | Total cost of the API call | | `persons_usd` / `phones_usd` / `companies_usd` | Cost split by enrichment type | | `persons_count` / `phones_count` / `companies_count` | Records returned per enrichment type | | `balance_remaining_usd` | Remaining account balance, or amount due on pay-as-you-go plans | More than one `*_usd` field can be non-zero in the same request. For example, if you run a Contact Enrichment with `include_phones: true` and we return both a contact record and a phone number, you'll see charges under both `persons_usd` and `phones_usd`. ## Cost mapping | Category | Endpoints | | ----------- | ------------------------------------------------------------------------------- | | `persons` | Contact Enrichment, Prospector, Search Contact | | `phones` | Phone Enrichment, or any Contact/Prospector request with `include_phones: true` | | `companies` | Company Enrichment, Company Reveal, Search Company | Usage is calculated using your currently configured rates. If your pricing changed recently, updated rates may not appear until any existing prepaid balance has been fully consumed. ## Agentic and automated pipelines `balance_remaining_usd` is returned on every call, making it straightforward to build cost-aware automation without polling the Account API. Common patterns: * **Budget enforcement**: halt a loop or batch job when balance drops below a threshold * **Top-up triggers**: fire an alert or webhook when balance crosses a floor * **Per-run cost tracking**: sum `total_usd` across calls to measure spend per job or workflow * **Self-regulating agents**: let an LLM-orchestrated pipeline decide whether to continue enriching based on remaining balance ```python theme={null} response = waterfall.enrich_contact(...) remaining = response["output"]["usage"]["balance_remaining_usd"] if remaining < 10.00: trigger_top_up_alert() ``` The cost data is already in the response. No extra round-trip needed. ## Examples by endpoint [Contact Enrichment Launcher](/v1/contact-enrichment-launcher) ```json theme={null} { "output": { "person": { ... }, "usage": { "total_usd": 0.1, "phones_usd": 0.0, "phones_count": 0, "persons_usd": 0.1, "persons_count": 1, "companies_usd": 0.0, "companies_count": 0, "balance_remaining_usd": 717.486 } } } ``` [Phone Enrichment Launcher](/v1/phone-enrichment-launcher) ```json theme={null} { "output": { "person": { ... }, "usage": { "total_usd": 0.1, "phones_usd": 0.1, "phones_count": 1, "persons_usd": 0.0, "persons_count": 0, "companies_usd": 0.0, "companies_count": 0, "balance_remaining_usd": 717.486 } } } ``` [Prospector Launcher](/v1/prospector-launcher) ```json theme={null} { "output": { "persons": [], "usage": { "total_usd": 0.5, "phones_usd": 0.1, "phones_count": 1, "persons_usd": 0.4, "persons_count": 4, "companies_usd": 0.0, "companies_count": 0, "balance_remaining_usd": 717.486 } } } ``` [Search Contact Launcher](/v1/search-contact-launcher) ```json theme={null} { "output": { "persons": [], "usage": { "total_usd": 0.75, "phones_usd": 0.0, "phones_count": 0, "persons_usd": 0.75, "persons_count": 15, "companies_usd": 0.0, "companies_count": 0, "balance_remaining_usd": 717.486 } } } ``` [Search Company Launcher](/v1/search-company-launcher) ```json theme={null} { "output": { "companies": [], "usage": { "total_usd": 0.75, "phones_usd": 0.0, "phones_count": 0, "persons_usd": 0.0, "persons_count": 0, "companies_usd": 0.75, "companies_count": 15, "balance_remaining_usd": 717.486 } } } ``` [Email Verifier](/v1/email-verifier) ```json theme={null} { "output": { "email": { ... }, "usage": { "total_usd": 0.1, "phones_usd": 0.0, "phones_count": 0, "persons_usd": 0.1, "persons_count": 1, "companies_usd": 0.0, "companies_count": 0, "balance_remaining_usd": 717.486 } } } ``` [Job Change Launcher](/v1/job-change-launcher) ```json theme={null} { "output": { "job_change_status": "moved", "person": { ... }, "usage": { "total_usd": 0.1, "phones_usd": 0.0, "phones_count": 0, "persons_usd": 0.1, "persons_count": 1, "companies_usd": 0.0, "companies_count": 0, "balance_remaining_usd": 717.486 } } } ``` [Company Reveal Launcher](/v1/company-reveal-launcher) ```json theme={null} { "output": { "company": { ... }, "usage": { "total_usd": 0.1, "phones_usd": 0.0, "phones_count": 0, "persons_usd": 0.0, "persons_count": 0, "companies_usd": 0.1, "companies_count": 1, "balance_remaining_usd": 717.486 } } } ``` [Company Enrichment Launcher](/v1/company-enrichment-launcher) ```json theme={null} { "output": { "company": { ... }, "usage": { "total_usd": 0.1, "phones_usd": 0.0, "phones_count": 0, "persons_usd": 0.0, "persons_count": 0, "companies_usd": 0.1, "companies_count": 1, "balance_remaining_usd": 717.486 } } } ``` # Introduction Source: https://docs.waterfall.io/v1/introduction Complete API reference for Waterfall — find and enrich contacts, prospect by title and company, verify emails, and more. The Waterfall API is available at `https://api.waterfall.io`. All requests require an API key in the `x-api-key` header. **Building with a coding agent** — Claude Code, Codex, Cursor, Devin, or similar — **or are one yourself?** Start at [Agent Skills](/v1/skills-overview) and point it at the [Waterfall API Skills repo](https://github.com/Waterfall-io/api-skills), whether you want to call the API directly or generate integration code you own. ## Endpoints | Endpoint | Type | Description | | ----------------------------------------------------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | [Prospector](/v1/prospector-launcher) | Async | Find contacts at a target company by title and location filters. Returns emails, phones (optional), and employment history. | | [Search Contact](/v1/search-contact-launcher) | Sync | Search contacts by company identity, title, department, seniority, or hire date. Returns profile data — contact channels not included. | | [Search Company](/v1/search-company-launcher) | Sync | Search companies by industry, headquarters country, and headcount range. | | [Contact Enrichment](/v1/contact-enrichment-launcher) | Async | Enrich a contact by LinkedIn, email, or name + domain. Returns verified email, phone (optional), title, and employment history. | | [Phone Enrichment](/v1/phone-enrichment-launcher) | Async | Find mobile and direct-dial numbers for a contact. | | [Company Enrichment](/v1/company-enrichment-launcher) | Async | Enrich a company by domain, LinkedIn, or name. Returns full company profile. | | [Company Reveal](/v1/company-reveal-launcher) | Sync | Reveal company information from an IPv4 address. Beta. | | [Company Titles](/v1/company-titles-launcher) | Sync | Get all active job titles at a company by domain or LinkedIn. Ideal for flexible title targeting and use alongside Search Contact title lists. Beta. | | [Job Change Detection](/v1/job-change-launcher) | Sync | Check if a contact is still at a given company. Returns status and a profile snapshot. | | [Email Verification](/v1/email-verifier) | Sync | Verify an email address and return deliverability status. | | [Webhook JWKS](/v1/webhook-jwks) | Sync | Fetch Ed25519 public keys for verifying signed webhook deliveries. | | [Account Reporting](/v1/account-reporter-v2) | Sync | Retrieve usage counters at the account and key level. | | [API Key Management](/v1/api-keys-overview) | Sync | Create and manage sub-keys under a master key. | # Job Change Finder Source: https://docs.waterfall.io/v1/job-change-finder api_specs.yaml GET /v1/job/change Re-fetch a previous Job Change result by job_id. Job Change returns results synchronously from the launcher. Use this endpoint to **re-fetch or audit** a previous job — not to poll for completion. ## Job change status values | Value | Meaning | | ----------- | -------------------------------------------------------------------- | | `moved` | Contact has started a new role at a different company. | | `left` | Contact appears to have left the company with no current role found. | | `no_change` | Contact is still at the input company. | | `unknown` | Profile could not be verified or no reliable update was found. | When `job_change_status` is `moved` or `left`, `output.person` contains the contact's current profile with their new role and employer. When `no_change` or `unknown`, `output.person` is an empty object `{}`. # Job Change Launcher Source: https://docs.waterfall.io/v1/job-change-launcher api_specs.yaml POST /v1/job/change Detect whether a contact has changed jobs using company and contact identifiers. **Job Change Detection Overview** **You input:** Previous company identifier (domain or LinkedIn) + contact identifier (LinkedIn, email, or name) **You get:** Job change status — `moved`, `left`, `no_change`, or `unknown` — and a profile snapshot when available Job Change returns results **synchronously** — status and output are included directly in the response. ## Supported input combinations Provide as many fields as you have — more signals increase match accuracy. | Combination | Required fields | Notes | | ------------------------------------- | ----------------------------------------- | ------------------------------ | | Domain + contact LinkedIn | `company_domain` + `contact_linkedin` | Highest coverage. Recommended. | | Domain + professional email | `company_domain` + `professional_email` | — | | Domain + personal email | `company_domain` + `personal_email` | — | | Company LinkedIn + contact LinkedIn | `company_linkedin` + `contact_linkedin` | — | | Company LinkedIn + professional email | `company_linkedin` + `professional_email` | — | | Professional email only | `professional_email` | Minimum viable input. | `company_domain` + `contact_linkedin` yields the highest coverage. When possible, include both. ## Business notes Add your internal notes here — pricing, use cases, etc. # Open API Specification Source: https://docs.waterfall.io/v1/openapi-spec Download the Waterfall API OpenAPI specification. The full OpenAPI 3.1 specification for the Waterfall API is available for download. Use it to generate client SDKs, import into Postman, or explore the schema. Download `api_specs.yaml` # Phone Enrichment Finder Source: https://docs.waterfall.io/v1/phone-enrichment-finder api_specs.yaml GET /v1/enrichment/phone Retrieve phone enrichment job state and output by job_id. Use the `job_id` returned by [Phone Enrichment Launcher](/v1/phone-enrichment-launcher) to fetch state and results. ## Polling vs webhook If you passed `webhook_url` when launching the job, you don't need to call this endpoint at all — Waterfall will automatically deliver the result to your URL as soon as the job is ready. If you didn't use a webhook, poll this endpoint until `status` is `SUCCEEDED`: ``` GET /v1/enrichment/phone?job_id={job_id} ``` Use short exponential backoff — most jobs complete in under 15 seconds. Start with a 2s delay, then 4s, then 8s. ## Job states | Status | Meaning | | ----------- | -------------------------------------------------- | | `RUNNING` | Job is still processing. Continue polling. | | `SUCCEEDED` | Job is complete. Phone data is in `output.person`. | ## Phone number fields | Field | Description | | --------------- | -------------------------------------------------------------- | | `mobile_phone` | Primary phone number in E.164 format, or `null` if not found. | | `phone_numbers` | Full list of numbers found. Empty array `[]` if none returned. | A `SUCCEEDED` status does not guarantee a phone number was found — it means the job completed. Always check whether `mobile_phone` is non-null or `phone_numbers` is non-empty before billing logic. ## Response shape The `input.task` object always echoes back all fields from the original launch request. Fields not provided will be `null`. # Phone Enrichment Launcher Source: https://docs.waterfall.io/v1/phone-enrichment-launcher api_specs.yaml POST /v1/enrichment/phone Get mobile and direct line phone numbers by LinkedIn, email, or name plus domain. **Phone Enrichment Overview** **You input:** LinkedIn URL/slug, email, or name + domain **You get:** Mobile and direct-dial phone numbers in E.164 format This is an asynchronous endpoint. Use the returned `job_id` with [Phone Enrichment Finder](/v1/phone-enrichment-finder) to retrieve results. We recommend passing `webhook_url` for a smoother integration — Waterfall will POST the result directly to your endpoint as soon as the job is ready, so you don't need to poll. You are only charged when a phone number is successfully returned. ## Input strategies | Priority | Strategy | Required fields | Notes | | -------- | ------------------- | ------------------------------------- | ----------------------------------------------------------- | | 1 | LinkedIn | `linkedin` | Significantly better results. Use full URL or slug. | | 2 | Email | `email` | Professional email gives higher success rate than personal. | | 3 | Full name + domain | `full_name` + `domain` | Fallback when no LinkedIn or email available. | | 4 | Split name + domain | `first_name` + `last_name` + `domain` | Same as above, split across two fields. | If a previous attempt with LinkedIn was unsuccessful, retry with a professional email — the two methods use different underlying data sources. ## Business notes Add your internal notes here — pricing tier, credit cost per successful number, SLA guarantees, etc. * **Billing**: charged per phone number returned, not per launch * **Format**: all numbers returned in E.164 format (e.g. `+14155550123`) * **Coverage**: `mobile_phone` returns the primary number; `phone_numbers` is the full list ## Next step After launching, call [Phone Enrichment Finder](/v1/phone-enrichment-finder) with the `job_id`: ``` GET /v1/enrichment/phone?job_id={job_id} ``` # Prospector Finder Source: https://docs.waterfall.io/v1/prospector-finder api_specs.yaml GET /v1/prospector Retrieve Prospector job status, company data, and contacts by job_id. Use the `job_id` returned by [Prospector Launcher](/v1/prospector-launcher) to fetch state and results. ## Polling vs webhook If you passed `webhook_url` when launching the job, you don't need to call this endpoint at all — Waterfall will automatically POST the result to your URL as soon as the job is ready. If you didn't use a webhook, poll this endpoint until `status` is `SUCCEEDED`: ``` GET /v1/prospector?job_id={job_id} ``` Use short exponential backoff — start with a 5s delay, then 10s, then 20s. ## Job states | Status | Meaning | | ----------- | ------------------------------------------ | | `RUNNING` | Job is still processing. Continue polling. | | `SUCCEEDED` | Job is complete. Results are in `output`. | ## What you get The `output` object contains two keys: | Field | Description | | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `output.company` | Company profile for the target domain — name, LinkedIn, size, website | | `output.persons` | Array of contacts found. Each person uses the standard [Person object](/v1/contact-enrichment-finder) with full fields: email, title, location, seniority, experiences, and more | The number of persons returned is controlled by the `limit` parameter set at launch (default `10`, max `500`). # Prospector Launcher Source: https://docs.waterfall.io/v1/prospector-launcher api_specs.yaml POST /v1/prospector Launch a contact search using domain, LinkedIn URL/ID, or company name. **Prospector Overview** **You input:** Target account domain or LinkedIn URL, plus title filters (Boolean expression or named lists) **You get:** Contacts with verified emails, job titles, location, LinkedIn profiles, and phone numbers (optional) ## Supported input combinations You can launch Prospector in two main modes: | Use case | Required fields | Notes | | ---------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------- | | Single Boolean filter | `domain` **and** `title_filter` | Best when you already have a well-defined Boolean title expression. | | Multiple named filters | `domain` **and** `title_filters` (1–5) | Use when you want to try several personas (for example founders, sales, marketing) in priority order. | | Exact title lists | `domain` **and** `title_lists` (1–5) | Use when you have a curated set of exact titles to match against. | # Prospector location filter Source: https://docs.waterfall.io/v1/prospector-location-filter How location_country, location_countries, and location_name work. Prospector supports three location parameters. They can be used individually or combined. ## `location_country` Exact country match. Use full country names — partial values are not valid. ```json theme={null} { "domain": "acme.com", "title_filter": "head of sales OR vp sales", "location_country": "United States" } ``` ## `location_countries` Use when you need to target multiple countries. Overrides `location_country` when present and non-empty. ```json theme={null} { "domain": "acme.com", "title_filter": "head of sales OR vp sales", "location_countries": ["United States", "Canada", "United Kingdom"] } ``` ## `location_name` Free-text location string. Accepts full or partial values — city, region, or country. Waterfall matches contacts whose location contains the string. ```json theme={null} { "domain": "acme.com", "title_filter": "head of sales OR vp sales", "location_name": "California, United States" } ``` Valid partial forms: | Input | Matches | | ------------------------------------------ | ------------------------------------------ | | `San Francisco, California, United States` | City-level — San Francisco only | | `California, United States` | State-level — all of California | | `United States` | Country-level — same as `location_country` | | `Paris` | Matches any location containing "Paris" | Region-level filtering (state or province) gives the best balance of precision and coverage. City-level can reduce results significantly. ## Combining filters `location_name` and `location_countries` can be used together. For example, to find contacts in California or anywhere in France: ```json theme={null} { "domain": "acme.com", "title_filter": "head of sales OR vp sales", "location_name": "California", "location_countries": ["France"] } ``` # Prospector multiple title filters Source: https://docs.waterfall.io/v1/prospector-multiple-title-filters Use ordered `title_filters` arrays for fallback targeting. Use `title_filters` when you need multiple prioritized filter strategies. Waterfall tries filters in the provided order until it reaches the requested `limit`. ```json theme={null} "title_filters": [ { "name": "ceo level", "filter": "ceo OR founder OR owner" }, { "name": "sales", "filter": "revenue OR growth OR sales" }, { "name": "marketing", "filter": "marketing OR operations" } ] ``` ## Guidance * Put highest-value persona first. * Keep filter names human-readable for debugging. ## title\_lists — exact title matching Use `title_lists` when you have a curated set of exact titles rather than a boolean expression. Each entry in `titles` is matched exactly against the contact's title. ```json theme={null} "title_lists": [ { "name": "founders", "titles": ["Founder", "Co-Founder", "CEO"] }, { "name": "sales", "titles": ["VP of Sales", "Head of Sales", "Director of Sales"] } ] ``` Up to 5 lists per request. Lists are evaluated in order — same priority behavior as `title_filters`. **`title_filters` vs `title_lists`** | | `title_filters` | `title_lists` | | ----------- | ---------------------------- | ---------------------------- | | Matching | Boolean / keyword expression | Exact title string | | Flexibility | High — catches variations | Low — must match precisely | | Best for | Broad persona targeting | Known, controlled title sets | Only one of `title_filter`, `title_filters`, or `title_lists` is used per request — they are mutually exclusive. # Prospector title filter Source: https://docs.waterfall.io/v1/prospector-title-filter How to build boolean expressions for `title_filter`. Use `title_filter` to target specific personas with boolean logic. ## Rules * Operators must be uppercase: `AND`, `OR`, `NOT`. * Use spaces around operators. * Use parentheses for clarity and precedence. * If `title_filter` is missing, API can use `title_filters`. ## Valid examples * `manager` * `manager AND NOT project manager` * `(accountant OR accounting) AND NOT intern` ## Invalid examples * `managerANDdirector` (missing spaces around operator) * `manager NOT (project manager)` (invalid expression form) # Rate limit Source: https://docs.waterfall.io/v1/rate-limit Rate limit behavior and response headers. All endpoints are rate-limited, except Account endpoints. If you hit limits frequently, contact your account manager for capacity planning. ## Headers you may receive | Header | Meaning | | ---------------------- | -------------------------------- | | `X-RateLimit-Limit` | Max requests allowed in interval | | `X-RateLimit-Interval` | Interval duration in seconds | | `Retry-After` | Seconds to wait before retry | ## Retry guidance * Respect `Retry-After` when present. * Use exponential backoff for repeated retries. # Search Company Finder Source: https://docs.waterfall.io/v1/search-company-finder api_specs.yaml GET /v1/search/company Re-fetch a previous Search Company job by job_id. Search Company returns results synchronously from the launcher. Use this endpoint to **re-fetch or audit** a previous job — not to poll for completion. ## What you get The response mirrors the launcher response exactly: | Field | Description | | ------------------ | ---------------------------------------------------------------- | | `input.task` | Echo of all request fields plus `job_id` and `context_id` | | `output.companies` | Array of matched companies — same shape as the launcher response | # Search Company Launcher Source: https://docs.waterfall.io/v1/search-company-launcher api_specs.yaml POST /v1/search/company Search for companies by industry, location, and size. **Search Company Overview** **You input:** Industry, headquarters country, or headcount range (at least one required) **You get:** Company profiles with domain, LinkedIn, employee count, industry, HQ location, and funding details Search Company returns results **synchronously** — `output.companies` is included directly in the response. There is no separate polling step. ## Filters At least one of the following must be provided: | Field | Description | | -------------------- | ------------------------------------------------------------------------------------------------- | | `industries` | LinkedIn industry categories (e.g. `software development`, `information technology and services`) | | `location_countries` | Countries where the company is headquartered | | `sizes` | LinkedIn employee-size buckets (e.g. `201-500`, `501-1000`) | All three can be combined — results must match all provided filters. ## Output Each company in `output.companies` includes name, domain, LinkedIn, employee count, industry, founding year, address, and funding details where available. # Search Contact filters Source: https://docs.waterfall.io/v1/search-contact-filters How title filters, departments, seniorities, and location work. ## Title filters Two syntaxes are supported — pick one per request. **`title_filters`** — boolean expression per named group. Supports `OR`, `AND`, `NOT`: ```json theme={null} { "domain": "acme.com", "title_filters": [ { "name": "sales_leaders", "filter": "head of sales OR vp sales OR director of sales" } ] } ``` **`title_lists`** — explicit list of titles per named group. Useful when you have a curated set of exact titles: ```json theme={null} { "domain": "acme.com", "title_lists": [ { "name": "sales_leaders", "titles": ["Head of Sales", "VP of Sales", "Director of Sales"] } ] } ``` Up to 5 groups per request. Multiple groups act as OR — a contact matching any group is included. ## Departments and seniorities Optional filters that narrow results within any search mode. **`departments`** — allowed values: `Leadership`, `Engineering`, `Product Management`, `Sales`, `Marketing`, `Customer Success`, `Finance`, `Accounting`, `Legal`, `Human Resources`, `Information Technology`, `Operations`, `Education`, `Other` **`seniorities`** — allowed values: `Owner`, `CXO`, `Partner`, `Vice President`, `Director`, `Manager`, `Senior`, `Entry`, `Other` ```json theme={null} { "domain": "acme.com", "departments": ["Sales", "Marketing"], "seniorities": ["Director", "Vice President"] } ``` ## Location (contact-level) `location_countries` filters contacts by where they are located — not where the company is headquartered. ```json theme={null} { "domain": "acme.com", "title_filters": [ { "name": "sales_leaders", "filter": "head of sales OR vp sales" } ], "location_countries": ["United States", "Canada"] } ``` For company-set searches, use `company_location_countries` to filter by company HQ country and `location_countries` to filter by the contact's own location. ## Name filters | Field | Behavior | | ---------------- | ------------------------------------------------------------------------------ | | `included_names` | Restrict results to contacts whose full name matches one of the listed strings | | `excluded_names` | Remove contacts whose full name matches one of the listed strings | Useful when you want to target or skip specific individuals in a company search. # Search Contact Finder Source: https://docs.waterfall.io/v1/search-contact-finder api_specs.yaml GET /v1/search/contact Re-fetch a previous Search Contact job by job_id. Search Contact returns results synchronously from the launcher. Use this endpoint to **re-fetch or audit** a previous job — not to poll for completion. ## When to use this Pass the `job_id` returned by [Search Contact Launcher](/v1/search-contact-launcher) to retrieve the same job state, original input echo, and `output.persons` without re-running the search. ## What you get The response mirrors the launcher response exactly: | Field | Description | | ---------------- | ------------------------------------------------------------- | | `input.task` | Echo of all request fields plus `job_id` and `context_id` | | `output.persons` | Array of contacts found — same shape as the launcher response | Contact channels (`professional_email`, `mobile_phone`, etc.) are always `null` in Search Contact output. Use [Contact Enrichment](/v1/contact-enrichment-launcher) to retrieve them. # Search Contact Launcher Source: https://docs.waterfall.io/v1/search-contact-launcher api_specs.yaml POST /v1/search/contact Search for contacts by LinkedIn, company identity, or persona filters. **Search Contact Overview** **You input:** LinkedIn URL, company identity, or persona filters (title, department, seniority, location, hire date) **You get:** Contact profiles with name, title, company, location, and employment history — no contact channels Search Contact returns results **synchronously** — `output.persons` is included directly in the response. There is no separate polling step. This endpoint doesn't return contact channels (email, phone). It returns profile data only. To retrieve contact channels for a person, pass their `linkedin_url` as the `linkedin` field in [Contact Enrichment Launcher](/v1/contact-enrichment-launcher). ## Search modes | Mode | Anchor field(s) | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Single contact by LinkedIn | `contact_linkedin` | | Company-specific search | `domain` **or** `company_linkedin` **or** `company_name` — find contacts at a specific company, filtered by title, department, seniority, location, and more | | Company-set persona search | One of `company_location_countries` / `company_industries` / `company_employee_ranges` **+** an experience date **+** a title filter — discover personas across a set of companies | For details on title filters, experience dates, and department/seniority filters, see [Search Contact Filters](/v1/search-contact-filters). # Use the API with an agent Source: https://docs.waterfall.io/v1/skills-direct-api Have your agent call the Waterfall API directly over raw HTTP — auth, request shapes, pagination, and errors. `waterfall-direct-api` teaches your agent to call the Waterfall API directly over raw HTTP, in whatever language or tool is already at hand. It carries auth, request/response shapes, pagination, and error handling. ## When to reach for it Use it when you want to **use the API now**: * An ad hoc lookup or one-off enrichment * A quick script or notebook * Exploring the data before you commit to anything * An agent making live calls as part of a task Nothing is written into your codebase, so there's nothing to maintain afterward. ## Install ```bash theme={null} npx skills add Waterfall-io/api-skills --skill waterfall-direct-api ``` Then point your agent at the repo and have it **fetch that link** so it works from the current skill definition: **[https://github.com/Waterfall-io/api-skills](https://github.com/Waterfall-io/api-skills)** ## Source of truth The skill tracks this documentation and the public OpenAPI spec. If its guidance ever conflicts with the live docs, the docs win. Ready to build something durable instead? See [Integration](/v1/skills-integration). # Build an integration with an agent Source: https://docs.waterfall.io/v1/skills-integration Have your agent generate client-owned Waterfall integration code that lives in your repo and matches your stack. `waterfall-integration` has your agent generate client-owned integration code (Python or JavaScript/TypeScript) that lives in your own repo, written to match your stack and conventions. ## When to reach for it Use it when you're **building Waterfall into a product or workflow** and want durable integration code you own and maintain: * A service or job that calls Waterfall as part of your product * Reusable client code your team maintains over time * Code that has to follow your existing stack, patterns, and conventions Unlike [Direct API](/v1/skills-direct-api), this produces real code in your repo — you own it and maintain it going forward. ## Install ```bash theme={null} npx skills add Waterfall-io/api-skills --skill waterfall-integration ``` Then point your agent at the repo and have it **fetch that link** so it works from the current skill definition: **[https://github.com/Waterfall-io/api-skills](https://github.com/Waterfall-io/api-skills)** ## Source of truth The skill tracks this documentation and the public OpenAPI spec. If its guidance ever conflicts with the live docs, the docs win. Just need live calls without maintaining code? See [Direct API](/v1/skills-direct-api). # Agent Skills Source: https://docs.waterfall.io/v1/skills-overview Drop-in agent skills for the Waterfall API. Point your agent at the repo and let it do the rest. Building with an AI agent? Point it at the **[Waterfall API Skills repo](https://github.com/Waterfall-io/api-skills)** and have it **fetch that link**. The repo is kept in sync with the live API and carries everything an agent needs — auth, request/response shapes, pagination, and error handling. **Repo:** [https://github.com/Waterfall-io/api-skills](https://github.com/Waterfall-io/api-skills) There are two skills, installed independently. Pick by what you're actually doing: | Skill | Use it when | Guide | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | | `waterfall-direct-api` | You want to **use the API now** — an ad hoc lookup, a script, exploring data, or an agent making live calls. Nothing to maintain afterward. | [Direct API](/v1/skills-direct-api) | | `waterfall-integration` | You're **building Waterfall into a product or workflow** and want durable integration code you own and maintain. | [Integration](/v1/skills-integration) | ## Source of truth The repo tracks this documentation and the public OpenAPI spec. If a skill's guidance ever conflicts with the live docs, the docs win. Have your agent fetch the repo to pull the current skill definitions: **[https://github.com/Waterfall-io/api-skills](https://github.com/Waterfall-io/api-skills)** # Webhook JWKS Source: https://docs.waterfall.io/v1/webhook-jwks api_specs.yaml GET /.well-known/jwks.json Fetch Waterfall's Ed25519 public keys for verifying webhook signatures. Use the keys returned here to verify the `X-Webhook-Signature` header on Waterfall webhook deliveries. See [Webhook Verification](/v1/webhook-verification) for the full verification protocol and ready-to-copy verifier code. # Webhook Verification Source: https://docs.waterfall.io/v1/webhook-verification Verify Waterfall webhook deliveries using Ed25519 signatures. Webhook signing applies to every endpoint that accepts `webhook_url`. Each signed delivery includes an `X-Webhook-Signature` header containing a compact JWT. Your server uses this to confirm the delivery came from Waterfall and that the body was not modified in transit. If signing configuration is unavailable, Waterfall delivers the webhook without the `X-Webhook-Signature` header to preserve backward compatibility. ## How it works Waterfall signs each webhook delivery with an Ed25519 private key and puts the result in `X-Webhook-Signature` as a compact JWT. The body payload is not modified. The JWT contains a `body_hash` claim: the unpadded base64url SHA-256 digest of the exact raw request body bytes. Verification has three parts: valid signature, valid claims, matching body hash. ## JWKS endpoint Waterfall publishes its Ed25519 public keys at: ``` GET https://api.waterfall.io/.well-known/jwks.json ``` Cache the response for 5 minutes (`Cache-Control: public, max-age=300`). During key rotation, old and new keys overlap for at least the 5-minute cache window so in-flight deliveries continue to verify. See the [Webhook JWKS](/v1/webhook-jwks) reference for the full response schema. ## Verification steps Follow these steps in order. Reject the request on any failure. ### 1. Read the signature header * Header name: `X-Webhook-Signature` * Header value: one compact JWT in `header.payload.signature` form * Reject if absent or larger than 8192 bytes ### 2. Decode the JWT Split on `.`. Base64url-decode each of the three segments (no padding). The protected header must contain: | Field | Required value | | ----- | ---------------- | | `alg` | `EdDSA` | | `typ` | `JWT` | | `kid` | non-empty string | ### 3. Fetch and cache public keys Fetch the JWKS and select the key where the JWKS `kid` matches the JWT `kid`. JWK shape: `kty: "OKP"`, `crv: "Ed25519"`, public key bytes in `x`. If the `kid` is not found in a cached response, refresh the JWKS once before rejecting. Throttle forced refreshes to one per 30 seconds. ### 4. Verify the signature Verify the Ed25519 signature over the ASCII bytes of `base64url(header) + "." + base64url(payload)`. ### 5. Validate claims | Claim | Type | Description | | --------------- | ------- | --------------------------------------------------- | | `iat` | integer | Issued-at timestamp (Unix seconds) | | `exp` | integer | Expiry. Always `iat + 900` (15 minutes). | | `jti` | string | Unique delivery ID. Use for replay deduplication. | | `job_id` | string | The Waterfall job this delivery belongs to. | | `body_hash` | string | Unpadded base64url SHA-256 of the raw request body. | | `body_hash_alg` | string | Always `sha-256`. | Validation rules: * `body_hash_alg` must be `sha-256` * `exp` must equal `iat + 900` * `iat` must not be more than 300 seconds ahead of your server clock * Current time must be before `exp` ### 6. Hash the body and compare Hash the **raw request body bytes before any JSON parsing**. Do not reserialize. `body_hash` is the unpadded base64url SHA-256 digest of those exact bytes. If the signature is valid but the body hash does not match, reject the request. A mismatch means the body was modified in transit. ## Replay prevention JWT verification does not prevent replay attacks. Store each accepted `jti` for at least 15 minutes (the signature lifetime) and reject any delivery whose `jti` you have already processed. ## Error codes | Code | Cause | | ---------------------------- | ---------------------------------------------------- | | `missing_signature_header` | `X-Webhook-Signature` is absent | | `signature_header_too_large` | Header exceeds 8192 bytes | | `malformed_compact_jwt` | JWT does not have three dot-separated segments | | `malformed_jwt_segment` | A segment failed base64url decoding | | `invalid_alg` | `alg` is not `EdDSA` | | `invalid_typ` | `typ` is not `JWT` | | `missing_kid` | `kid` is empty or absent | | `jwks_fetch_failed` | JWKS endpoint returned an error | | `unknown_kid` | No JWKS key matches the JWT `kid` | | `invalid_signature` | Ed25519 signature verification failed | | `invalid_time_claims` | `iat` or `exp` is missing or malformed | | `invalid_jti` | `jti` is missing or malformed | | `invalid_job_id` | `job_id` is missing or malformed | | `invalid_body_hash` | `body_hash` is missing or malformed | | `unsupported_body_hash_alg` | `body_hash_alg` is not `sha-256` | | `invalid_expiry_window` | `exp` does not equal `iat + 900` | | `issued_in_future` | `iat` is more than 300 seconds ahead of server clock | | `expired_signature` | Current time is past `exp` | | `body_hash_mismatch` | Computed body hash does not match `body_hash` | ## Verification kit Waterfall provides a reference implementation with a single-file verifier for Python and TypeScript, designed to be copied directly into your app. [webhook-verification-kit on GitHub](https://github.com/Waterfall-io/webhook-verification-kit)