Public API Reference
Enflow Engine API Documentation
Enflow Engine API — Public Reference
The Enflow Engine API lets you enrich companies and people programmatically: get firmographic data for a company from its domain, find email addresses and LinkedIn profiles for people, verify emails, and more — all through a single API key.
- Base URL (production):
https://enflowpy.vercel.app/api/v1 - Interactive docs (Swagger):
https://enflowpy.vercel.app/api/v1/docs - OpenAPI schema:
https://enflowpy.vercel.app/api/v1/openapi.json
All endpoints return JSON. Requests and responses use Content-Type: application/json.
Quick start
- Create an account →
POST /auth/register. The response includes your Enflow Engine API key (shown only once). - Verify your email (recommended) → click the link in the verification email (or
POST /auth/verifywith the token). You can start using the API immediately, but verifying keeps your account fully activated. - Call any enrichment endpoint with your key in the
Api-Keyheader:
curl -X POST https://enflowpy.vercel.app/api/v1/companies/enrich \
-H "Content-Type: application/json" \
-H "Api-Key: ENFLOW_YOUR_API_KEY_HERE" \
-d '{"domain": "usaa.com"}'
Authentication
Enflow supports two ways to authenticate. Every enrichment and dashboard endpoint accepts either.
1. Enflow Engine API key (recommended for integrations)
Send your API key in the Api-Key header:
Api-Key: ENFLOW_your_api_key_here
Your API key is generated when you register and is only displayed once. If you lose it, regenerate it (see Regenerate API key). Each account has one active key at a time; generating a new one revokes the old.
2. JWT Bearer token (dashboard/UI sessions)
Log in via POST /auth/login to get access and refresh tokens, then send:
Authorization: Bearer <access_token>
Access tokens last 1 hour; refresh tokens last 7 days (use POST /auth/refresh to rotate).
Connect Enflow to n8n
Enflow works with both n8n Cloud and self-hosted n8n through the built-in HTTP Request node. No custom or community node is required.
Prerequisites
- An Enflow Engine API key
- An n8n workflow with an HTTP Request node
- Production base URL:
https://enflowpy.vercel.app/api/v1
Store the API key securely
Do not paste the API key directly into every node or put it in a URL/query parameter.
- In n8n, open Credentials → New credential.
- Choose Header Auth.
- Set Name to
Api-Key. - Set Value to your Enflow Engine API key.
- Save it as
Enflow APIand select that credential in each Enflow HTTP Request node.
For self-hosted n8n, an environment-backed credential or n8n external secrets integration is also suitable. Never expose the key in workflow exports committed to source control.
Import the ready-made core operations workflow
Download and import enflow-core-operations.workflow.json in n8n → Workflows → Import from File. It contains five independent production webhook flows:
| Operation | n8n production webhook path | Enflow endpoint |
|---|---|---|
| People search | POST /webhook/enflow/people/search |
POST /people/search |
| People enrichment | POST /webhook/enflow/people/enrich |
POST /people/enrich |
| Company search | POST /webhook/enflow/companies/search |
POST /companies/search |
| Company enrichment | POST /webhook/enflow/companies/enrich |
POST /companies/enrich |
| Email verification | POST /webhook/enflow/email/verify |
POST /email/verify |
After importing:
- Create the
Enflow APIHeader Auth credential as described above. - Open each of the five Enflow ... HTTP Request nodes and select that credential. Imported credential IDs are placeholders and never contain a secret.
- Save and activate the workflow.
- Replace
https://YOUR_N8N_HOSTin the examples below with your n8n hostname.
The workflow forwards each webhook JSON body to the matching Enflow endpoint. Its people-search branch includes a Format People Rows Code node that recognizes cleaned_people, people, results, and provider responses nested at raw_data.people, then emits one flat n8n item per person for a readable table. Other branches return Enflow's JSON response unchanged. It uses a 60-second request timeout and does not embed or return the API key.
People search
curl -X POST https://YOUR_N8N_HOST/webhook/enflow/people/search \
-H "Content-Type: application/json" \
-d '{
"companies": {"domains": ["stripe.com"]},
"people": {"jobTitles": ["Chief Financial Officer"], "countries": ["US"]},
"limit": 25
}'
People enrichment — provide an email, LinkedIn URL, or a supported bulk people array:
curl -X POST https://YOUR_N8N_HOST/webhook/enflow/people/enrich \
-H "Content-Type: application/json" \
-d '{"linkedin_url":"https://www.linkedin.com/in/example"}'
Company search
curl -X POST https://YOUR_N8N_HOST/webhook/enflow/companies/search \
-H "Content-Type: application/json" \
-d '{"filters":{"industries":["Financial Services"],"countries":["US"]},"limit":25}'
Company enrichment — use domain for one company or domains for supported bulk input:
curl -X POST https://YOUR_N8N_HOST/webhook/enflow/companies/enrich \
-H "Content-Type: application/json" \
-d '{"domain":"stripe.com"}'
Email verification — use email for one address or emails for supported bulk input:
curl -X POST https://YOUR_N8N_HOST/webhook/enflow/email/verify \
-H "Content-Type: application/json" \
-d '{"email":"person@example.com"}'
Protect these public n8n webhooks with n8n authentication, an API gateway, IP restrictions, or another access-control layer appropriate to your deployment. The Enflow credential protects the upstream API but does not authenticate callers to your n8n webhook.
Test the connection
Create an HTTP Request node with:
| Setting | Value |
|---|---|
| Method | GET |
| URL | https://enflowpy.vercel.app/api/v1/dashboard/stats |
| Authentication | Generic Credential Type → Header Auth → Enflow API |
| Response Format | JSON |
A 200 JSON response confirms connectivity and authentication. A 401 response means the Api-Key header is missing, expired, or incorrect.
n8n URL requirement: HTTP Request nodes require an absolute URL beginning with
https://orhttp://. Values under API discovery'sendpointsobject are relative paths for compatibility and cannot be used alone. Use the corresponding value underendpoint_urls, or prepend the production origin.Correct company-search URL:
https://enflowpy.vercel.app/api/v1/companies/searchIf an Endpoints node feeds the HTTP Request node, use this expression:
{{ $json.endpoint_urls.enrichment.companies_search }}For the older relative field, concatenate the origin explicitly:
https://enflowpy.vercel.app{{ $json.endpoints.enrichment.companies_search }}
Example: enrich a company
Configure an HTTP Request node:
| Setting | Value |
|---|---|
| Method | POST |
| URL | https://enflowpy.vercel.app/api/v1/companies/enrich |
| Authentication | Header Auth → Enflow API |
| Send Headers | Content-Type: application/json |
| Send Body | On |
| Body Content Type | JSON |
| Specify Body | Using JSON |
{
"domain": "{{ $json.domain }}"
}
Connect a Manual Trigger, Webhook, spreadsheet, CRM, or database node before it. Each incoming n8n item should contain a domain field. n8n executes the request once per item unless you aggregate several items into a supported bulk payload.
Example: search for people
Use POST https://enflowpy.vercel.app/api/v1/people/search with this JSON body:
{
"companies": {
"domains": ["{{ $json.domain }}"]
},
"people": {
"jobTitles": ["Chief Financial Officer"],
"countries": ["US"]
},
"limit": 50
}
Response placement can vary by provider. People may be in cleaned_people, people, results, or raw_data.people. The downloadable workflow handles all four shapes with its Format People Rows node and outputs one flat item per person. For a manually built workflow using the response shown in n8n, add Split Out with Field to Split Out set to raw_data.people, or use the formatter node from the downloadable workflow.
Example: enrich a person
Configure an HTTP Request node:
| Setting | Value |
|---|---|
| Method | POST |
| URL | https://enflowpy.vercel.app/api/v1/people/enrich |
| Authentication | Header Auth → Enflow API |
| Send Headers | Content-Type: application/json |
| Send Body | On |
| Body Content Type | JSON |
| Specify Body | Using JSON |
Use an email address:
{
"email": "{{ $json.email }}",
"include": {
"email": true,
"linkedInUrl": true,
"mobile": false,
"jobHistory": false
}
}
Or use a LinkedIn profile URL:
{
"linkedin_url": "{{ $json.linkedin_url }}",
"include": {
"email": true,
"linkedInUrl": true
}
}
For bulk input, aggregate incoming n8n items before the HTTP Request node and send:
{
"people": {{ JSON.stringify($json.people) }}
}
Each object in people must contain an email or linkedin_url supported by the endpoint. Validate that at least one identifier exists before sending the request. Large bulk requests may return a background-job identifier; follow the polling instructions below instead of creating a duplicate enrichment request.
Example: search for companies
Configure an HTTP Request node:
| Setting | Value |
|---|---|
| Method | POST |
| URL | https://enflowpy.vercel.app/api/v1/companies/search |
| Authentication | Header Auth → Enflow API |
| Send Headers | Content-Type: application/json |
| Send Body | On |
| Body Content Type | JSON |
| Specify Body | Using JSON |
Example using industry, country, and employee-count filters:
{
"filters": {
"industries": ["Financial Services"],
"countries": ["US"],
"employeeCount": {
"from": 50,
"to": 5000
}
},
"limit": 25
}
Example using dynamic domains from a previous n8n node:
{
"filters": {
"domains": {{ JSON.stringify($json.domains) }}
},
"limit": 25
}
Company rows may be returned in companies, results, or raw_data.companies, depending on the selected provider. Add a Split Out node for the returned array to display one company per n8n table row. If nextPageToken is present, use the pagination pattern below.
Example: verify an email
Configure an HTTP Request node with POST https://enflowpy.vercel.app/api/v1/email/verify, the Enflow API Header Auth credential, and this JSON body:
{
"email": "{{ $json.email }}"
}
For supported bulk input:
{
"emails": {{ JSON.stringify($json.emails) }}
}
Pass company-search domains into people search
Company search returns normalized domains at companyDomains; some provider responses also include the same array at raw_data.companyDomains. When the Company Search node directly feeds the People Search node, use:
{
"companies": {
"domains": {{ JSON.stringify($json.companyDomains || $json.raw_data.companyDomains) }}
},
"limit": 5,
"peoplePerCompany": 5
}
When referencing a node named Company Search from elsewhere in the workflow:
{
"companies": {
"domains": {{ JSON.stringify($('Company Search').first().json.companyDomains || $('Company Search').first().json.raw_data.companyDomains) }}
},
"limit": 5,
"peoplePerCompany": 5
}
Before execution, check the expression preview. Correct array output contains brackets and quoted strings:
"domains": ["nuveen.com", "carlyle.com"]
If the preview shows "domains": nuveen.com,carlyle.com, the array was interpolated as text and the body is invalid JSON.
Expressions and dynamic input
Use n8n expressions anywhere in the JSON body. Quote scalar string expressions, but serialize arrays and objects with JSON.stringify(...) when embedding them in Using JSON mode.
Scalar strings:
{
"email": "{{ $json.email }}",
"linkedin_url": "{{ $json.linkedin_url }}"
}
When a value may be absent, validate it with an If node before calling Enflow. This avoids preventable 400 Bad Request responses.
Paginated search workflows
People and company searches can return nextPageToken. To request another page, send it back as pageToken with the same filters:
{
"companies": {
"domains": ["{{ $('Input').item.json.domain }}"]
},
"limit": 50,
"pageToken": "{{ $json.nextPageToken }}"
}
Use an If node to check whether {{ $json.nextPageToken }} is non-empty, then loop back to the HTTP Request node. Add a page counter or maximum-iteration guard to prevent accidental infinite loops. Aggregate pages only after the token is empty.
Background jobs and polling
Large bulk requests may return an enrichmentID with status: processing instead of final rows. In that case:
- Add a Wait node (for example, 5–15 seconds).
- Call
GET https://enflowpy.vercel.app/api/v1/dashboard/jobs/{{ $json.enrichmentID }}/status. - If status is
PENDING,PROCESSING, orPAUSED, wait and poll again. - When status is
COMPLETED, callGET https://enflowpy.vercel.app/api/v1/dashboard/jobs/{{ $json.enrichmentID }}?page=1&limit=100. - If more rows exist, increment
pageuntil all results are collected. - Route
FAILEDorCANCELLEDto an error-handling branch.
Set a maximum poll count and timeout appropriate to the workflow. Do not poll continuously without a Wait node.
Error handling in n8n
For production workflows, enable Retry on Fail only for transient responses:
| Status | Recommended handling |
|---|---|
400 |
Fix or validate the request body; do not retry unchanged input. |
401 |
Check the stored Enflow credential. |
402 / 429 |
Wait, review provider quota, or retry later with backoff. |
503 |
Retry with exponential backoff; provider availability may be temporary. |
| Network timeout | Retry with a bounded attempt count and increasing delay. |
Use an Error Trigger workflow or the node's error output to record the original item, HTTP status, and response body. Avoid enabling unlimited retries because provider operations can consume quota.
Recommended production pattern
Trigger/Webhook
→ Validate required fields
→ HTTP Request (Enflow)
→ If background job: Wait + poll
→ Split Out result rows
→ Upsert into CRM/database
→ Error branch / dead-letter storage
Keep one Enflow credential per environment, use a separate workflow or environment variable for the base URL, and avoid logging API keys or full sensitive person records.
AI access via MCP
Important:
https://enflowpy.vercel.app/api/v1is a REST/OpenAPI base URL, not a remote MCP or OAuth endpoint. Do not paste it into an MCP URL connector. Enflow's MCP integration is a local stdio bridge: the MCP client must launchmcp/server.py, which then calls the REST API securely with your server-side API key.
Enflow ships an official MCP (Model Context Protocol) server so AI assistants like Claude Desktop and Cursor can call the Enflow Engine API directly as tools. You talk to the AI in plain language ("enrich usaa.com and find their marketing leads") and it performs the API calls for you.
What you get
The MCP server exposes 30 tools covering every section of this API:
| Group | Tools |
|---|---|
| Company | enrich_company, enrich_companies_bulk, search_companies, company_lookalike |
| People | enrich_person, enrich_people_bulk, search_people |
| Verification & intel | verify_email, verify_emails_bulk, website_status, website_social, website_people, iab_classify |
| AI operations | ai_company_intel, ai_lead_qualification, ai_content_extraction |
| Google Places | google_places_text_search, google_places_nearby_search, google_places_place_details |
| Reference data | reference_countries, reference_industries, reference_departments, reference_seniorities, reference_job_titles |
| Jobs | get_job_status, list_jobs |
Tools use your Enflow Engine API key and hit the same production REST base URL, so credits and limits apply exactly as documented above. The bridge reads the key from ENFLOW_API_KEY and sends it using the OpenAPI security scheme Api-Key: <key>. Keep this variable in the server/MCP process environment; never place it in browser code or client bundles.
The bridge does not automatically retry enrichment requests. If a request times out, its completion state may be unknown; do not blindly repeat it.
Prerequisites
- Python 3.10+
- Your Enflow Engine API key (from registration — see Regenerate API key if you lost it)
- The server files from the
mcp/directory of the Enflow repository (server.py,requirements.txt)
Quick start
# 1. Install dependencies
pip install -r mcp/requirements.txt
# 2. Set your API key
export ENFLOW_API_KEY="enflow_your_key_here"
# 3. Run the server (stdio) — or skip this: your AI client launches it for you
python mcp/server.py
Connect Claude Desktop
Open Claude Desktop → Settings → Developer → Edit Config and add to claude_desktop_config.json:
{
"mcpServers": {
"enflow": {
"command": "python",
"args": ["/absolute/path/to/mcp/server.py"],
"env": {
"ENFLOW_API_KEY": "enflow_your_key_here"
}
}
}
}
Restart Claude Desktop. The Enflow tools appear automatically — try: "Use Enflow to enrich usaa.com and verify the CEO's email."
Connect Cursor
In Cursor → Settings → MCP Servers, add a new server:
- Name:
enflow - Type:
command - Command:
python /absolute/path/to/mcp/server.py - Environment variables:
ENFLOW_API_KEY= your key
Environment variables
| Variable | Default | Description |
|---|---|---|
ENFLOW_API_KEY |
— | (Required) Your Enflow Engine API key |
ENFLOW_API_URL |
https://enflowpy.vercel.app/api/v1 |
REST API base URL (not an MCP URL; override for staging) |
RUN_ENFLOW_SMOKE_TEST |
false |
Must be true to permit the live Stripe smoke test |
Testing the server
cd mcp
ENFLOW_API_KEY="your_key" python -m mcp dev server.py
This launches the MCP Inspector so you can verify initialization, tool discovery, and tool calls before wiring the server into your AI client.
For an explicitly enabled live company-enrichment smoke test:
cd mcp
ENFLOW_API_KEY="your_key" RUN_ENFLOW_SMOKE_TEST=true python smoke_test.py
The smoke test enriches stripe.com and prints only non-sensitive fields. Normal unit tests mock all HTTP requests and never consume enrichment credits.
Authentication troubleshooting
- HTTP
401means the REST service was reached but the key was missing or invalid. Confirm the server process hasENFLOW_API_KEYand sends it as theApi-Keyheader. - An OAuth or JSON-RPC transport error during MCP initialization usually means the REST URL was incorrectly configured as an MCP server. Configure a local command that launches
mcp/server.pyinstead. - HTTP
429means the request was rate-limited. Wait before making a deliberate retry. - HTTP
500or503means the service or its providers are temporarily unavailable. Do not expose credentials while collecting diagnostics.
1. Account & registration
Register a user
Creates an account and returns your API key.
POST /api/v1/auth/register
Request body
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | ✅ | Account email (used for login). |
password |
string | ✅ | Password (min 8 chars recommended). |
organization_name |
string | ✅ | Your company/organization name. |
tier |
string | ❌ | free (default), starter, explorer, or pro. |
Example request
curl -X POST https://enflowpy.vercel.app/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "you@company.com",
"password": "sup3r-secret-password",
"organization_name": "Your Company",
"tier": "free"
}'
Example response — 200 OK
{
"user_id": 42,
"email": "you@company.com",
"api_key": "ENFLOW_7f9c...your_key...",
"message": "Registration successful. Please check your email to verify your account."
}
⚠️ Save the
api_keyimmediately — it is not returned again.
Errors
| Status | code |
Meaning |
|---|---|---|
| 400 | USER_EXISTS |
An account with that email exists. |
Verify email
POST /api/v1/auth/verify
{ "token": "verification-token-from-email" }
200 → {"message": "Email verified successfully"}. 400 → invalid/expired token.
Resend verification email
POST /api/v1/auth/resend-verification
{ "email": "you@company.com" }
Always returns {"message": "Verification email sent"} (does not reveal whether the account exists).
Login (JWT)
POST /api/v1/auth/login
{ "email": "you@company.com", "password": "sup3r-secret-password" }
Response — 200 OK
{
"access": "eyJhbGciOiJIUzI1NiIs...",
"refresh": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": 42,
"email": "you@company.com",
"role": "member",
"tier": "free",
"is_verified": true
}
}
Refresh access token
POST /api/v1/auth/refresh
{ "refresh": "eyJhbGciOiJIUzI1NiIs..." }
Returns a fresh access token (and a rotated refresh token).
Password reset
POST /api/v1/auth/password-reset/request → { "email": "you@company.com" }
POST /api/v1/auth/password-reset/confirm → { "uid": "...", "token": "...", "new_password": "..." }
Get profile
GET /api/v1/profile/
Auth: Api-Key or JWT Bearer. Returns your user info, tier, credits used, and the prefix of your active API key (the full key is only shown once).
Regenerate API key
POST /api/v1/profile/regenerate-key
Auth: JWT Bearer only. Revokes the current key and returns a new one (shown once):
{
"message": "API key regenerated successfully",
"api_key": "ENFLOW_new_key_here",
"warning": "Save this key now. You won't be able to see it again."
}
Redeem a platform code (AppSumo / lifetime deals)
POST /api/v1/profile/redeem
{ "code": "YOUR-CODE" }
Unlocks a tier and its monthly credits.
2. Company enrichment
Enrich a company — single
POST /api/v1/companies/enrich
Request body — provide domain or website:
{ "domain": "usaa.com" }
Example response — 200 OK
{
"operation": "company_enrichment",
"total": 1,
"successful": 1,
"failed": 0,
"results": [
{
"success": true,
"result": {
"name": "USAA Property and Casualty Insurance Group",
"domain": "usaa.com",
"website": "https://usaa.com",
"industry": "Retirement",
"employee_count": 4102,
"revenue": "$500–1000M",
"founded": 1922,
"hq_address": "...",
"linkedin_url": "https://linkedin.com/company/usaa",
"description": "...",
"keywords": ["auto insurance", "banking", "..."]
},
"input": { "domain": "usaa.com" },
"source": "Surfe"
}
]
}
Enrich companies — bulk
POST /api/v1/companies/enrich
{ "domains": ["usaa.com", "apple.com", "stripe.com"] }
Response is the same shape with one results[] entry per input domain (order preserved; failed domains have "success": false with an error). Up to 50 domains run synchronously and return inline; larger bulk requests create a background job (see Jobs).
Company search
Find companies in the provider's database using firmographic filters.
POST /api/v1/companies/search
Request body — filters is required (at least one filter). limit is optional (default 50, max 200).
{
"filters": {
"industries": ["Fintech", "Banking"],
"countries": ["US", "GB"],
"employeeCount": { "from": 100, "to": 5000 },
"revenue": { "from": 10000000, "to": 1000000000 }
},
"limit": 50
}
Filter reference (Surfe v2 schema)
| Filter | Type | Description |
|---|---|---|
industries |
string[] |
Industry names. |
countries |
string[] |
ISO country codes (US, GB, …). Full country names are also accepted and auto-converted. |
names |
string[] |
Exact company names. |
domains |
string[] |
Company domains (e.g., ["stripe.com"]). |
domainsExcluded |
string[] |
Exclude these domains. |
employeeCount |
{from, to} |
Headcount range. |
revenue |
{from, to} |
Annual revenue range (USD). |
⚠️
filtersis passed through to whichever external provider is configured for your plan. The keys above follow the Surfe v2 schema; filter names for other providers (e.g., Apollo) may differ.
Example response — 200 OK (provider-native shape, not the enrichment envelope)
{
"companies": [
{
"name": "Stripe",
"domain": "stripe.com",
"website": "https://stripe.com",
"industries": ["Fintech", "Payments"],
"employeeCount": 8000,
"revenue": 10000000000,
"countries": ["US", "IE"]
}
],
"companyDomains": ["stripe.com"],
"total": 1,
"nextPageToken": "...",
"pageToken": "",
"limit": 50
}
Pagination — pass the returned nextPageToken as pageToken on the next request to fetch the following page:
{ "filters": { "industries": ["Fintech"] }, "limit": 50, "pageToken": "..." }
Code example
curl -X POST https://enflowpy.vercel.app/api/v1/companies/search \
-H "Content-Type: application/json" \
-H "Api-Key: ENFLOW_your_api_key_here" \
-d '{"filters": {"industries": ["Fintech"], "countries": ["US"]}, "limit": 20}'
Company lookalike
POST /api/v1/companies/lookalike
{
"domain": "stripe.com",
"filters": { "industries": ["Fintech"] }
}
Returns similar companies to the reference domain.
3. People enrichment
Enrich a person — by LinkedIn URL or email
POST /api/v1/people/enrich
{ "linkedin_url": "https://www.linkedin.com/in/jane-doe" }
or
{ "email": "jane.doe@company.com" }
Example response — 200 OK
{
"operation": "people_enrichment",
"total": 1,
"successful": 1,
"failed": 0,
"results": [
{
"success": true,
"result": {
"full_name": "Jane Doe",
"first_name": "Jane",
"last_name": "Doe",
"email": "jane.doe@company.com",
"job_title": "VP of Marketing",
"company_name": "Acme Inc",
"company_domain": "acme.com",
"linkedin_url": "https://www.linkedin.com/in/jane-doe",
"phone": "+1 555-0100"
},
"input": { "linkedin_url": "https://www.linkedin.com/in/jane-doe" },
"source": "Surfe"
}
]
}
You can also pass "include" to control which fields are fetched:
{ "linkedin_url": "...", "include": { "email": true, "jobHistory": true, "linkedInUrl": true, "mobile": true } }
Enrich people — bulk
POST /api/v1/people/enrich
{
"people": [
{ "linkedinUrl": "https://www.linkedin.com/in/jane-doe" },
{ "email": "john@acme.com" }
]
}
Up to 100 people run synchronously; larger payloads create a background job (max 10,000 per request).
People search
Find people (contacts) using company and/or person filters.
POST /api/v1/people/search
Request body — at least one of companies or people is required. limit is optional (default 50, max 200).
{
"companies": {
"domains": ["usaa.com", "stripe.com"],
"employeeCount": { "from": 100, "to": 10000 }
},
"people": {
"jobTitles": ["Manager", "Director"],
"countries": ["US"],
"departments": ["Engineering", "Marketing"],
"seniorities": ["Manager", "Director"]
},
"limit": 50,
"peoplePerCompany": 5
}
Filter reference (Surfe v2 schema)
| Group | Filter | Type | Description |
|---|---|---|---|
companies |
domains |
string[] |
Target company domains. |
companies |
countries |
string[] |
ISO country codes (names auto-converted). |
companies |
domainsExcluded |
string[] |
Exclude these domains. |
companies |
employeeCount |
{from, to} |
Company headcount range. |
companies |
industries |
string[] |
Company industries. |
companies |
names |
string[] |
Company names. |
companies |
revenue |
{from, to} |
Company annual revenue range. |
people |
countries |
string[] |
Person's country (ISO codes). |
people |
departments |
string[] |
Departments (Engineering, Sales, …). |
people |
jobTitles |
string[] |
Job titles. |
people |
seniorities |
string[] |
Seniority levels. |
⚠️ Filter keys follow the configured external provider's schema (Surfe v2 shown above).
Example response — 200 OK (provider-native shape, not the enrichment envelope)
{
"people": [
{
"firstName": "Jane",
"lastName": "Doe",
"jobTitle": "VP of Marketing",
"companyName": "USAA",
"companyDomain": "usaa.com",
"linkedInUrl": "https://www.linkedin.com/in/jane-doe",
"country": "US",
"seniorities": ["VP"],
"departments": ["Marketing"]
}
],
"total": 1,
"nextPageToken": "...",
"pageToken": "",
"limit": 50
}
Pagination — pass nextPageToken as pageToken on the next request. Optionally cap people per company with peoplePerCompany. For Surfe searches with multiple companies.domains and a peoplePerCompany cap, Enflow queries each domain independently, merges and deduplicates the people, and applies limit globally so one domain cannot consume the entire result set.
Code example
curl -X POST https://enflowpy.vercel.app/api/v1/people/search \
-H "Content-Type: application/json" \
-H "Api-Key: ENFLOW_your_api_key_here" \
-d '{"companies": {"domains": ["usaa.com"]}, "people": {"jobTitles": ["CTO"]}, "limit": 20}'
4. Verification, website & intelligence
Email verification
POST /api/v1/email/verify
{ "email": "jane.doe@company.com" }
Bulk: { "emails": ["a@x.com", "b@y.com"] }
Website status
POST /api/v1/website-status
{ "domain": "usaa.com" }
Resolves the domain and returns the final URL details needed for downstream enrichment. Bulk: { "domains": ["a.com", "b.com"] } (creates a job).
Website social (social link discovery)
POST /api/v1/website-social/enrich
{ "domain": "usaa.com" }
Scrapes the site and returns discovered social URLs (LinkedIn, Twitter/X, Facebook, Instagram, …). Bulk: { "domains": [...] } — up to 20 domains sync.
Website people (people on a site)
POST /api/v1/website-people/enrich
{
"domain": "usaa.com",
"max_pages": 8,
"max_people": 50,
"execution_mode": "fast"
}
Crawls the site and extracts people (names, titles, emails, LinkedIn). Bulk: { "domains": [...] } — up to 10 domains sync.
IAB classification
POST /api/v1/iab/classify
{
"company_name": "Stripe",
"company_website": "stripe.com",
"company_description": "Payment processing platform"
}
Response
{
"primary_category": "Finance",
"secondary_categories": ["Payments", "Fintech"],
"confidence_score": 0.95,
"iab_categories": { "IAB17": "Finance" }
}
Google Places
POST /api/v1/google-places/text-search → { "query": "coffee shops", "location": "Austin, TX", "result_limit": 20 }
POST /api/v1/google-places/nearby-search → { "latitude": 30.26, "longitude": -97.74, "radius": 5000, "page_size": 10 }
POST /api/v1/google-places/place-details → { "place_id": "ChIJ..." }
AI operations
POST /api/v1/ai/company-intel → { "content": "stripe.com" } — AI company intelligence report
POST /api/v1/ai/lead-qualification → { "content": "...text..." } — AI lead scoring
POST /api/v1/ai/content-extraction → { "content": "...text..." } — AI structured extraction
All three accept optional "model" and "provider_id".
Response shapes for searches, verification, and website-status are returned as-is from the configured provider (not wrapped in the
{operation, total, results[]}enrichment envelope). Enrichment endpoints (/companies/enrich,/people/enrich,/website-social/enrich,/website-people/enrich) always use the envelope.
5. Jobs & dashboard
Bulk operations above the synchronous caps return a job that runs in the background:
{
"enrichmentID": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "processing",
"total": 120,
"message": "Bulk enrichment job created for 120 companies"
}
| Endpoint | Method | Description |
|---|---|---|
/dashboard/jobs |
GET | List your jobs from the last 7 days. |
/dashboard/jobs/{id} |
GET | Full job detail incl. paginated results (page, limit). |
/dashboard/jobs/{id}/status |
GET | Lightweight status for polling. |
/dashboard/jobs/{id}/cancel |
POST | Cancel a PENDING / PROCESSING / PAUSED job. |
/dashboard/jobs/{id}/companies |
GET | Enriched company rows for a job. |
/dashboard/jobs/{id} |
DELETE | Delete a COMPLETED / FAILED / CANCELLED job. |
/dashboard/stats |
GET | Usage stats: credits used, success rate, job counts. |
Job statuses: PENDING → PROCESSING → COMPLETED (or FAILED / CANCELLED).
Error responses
Non-2xx responses return JSON with a detail (and often a machine-readable code):
{ "detail": "No active provider key available. Please add your own API key in Settings.", "code": "NO_PROVIDER_AVAILABLE" }
| Status | Common causes |
|---|---|
| 400 | Bad request, missing/invalid input, no provider available for the operation |
| 401 | Missing/invalid Api-Key header or JWT token |
| 404 | Requested resource not found |
| 503 | All providers exhausted (ALL_PROVIDERS_EXHAUSTED) or database overloaded (DATABASE_OVERLOADED) |
Credits, tiers & limits
- Every successful result consumes credits from your monthly plan (e.g., enrichment = 2 credits/result, search = 1/result).
- Tiers:
free(50 credits/mo),starter(500),explorer(2000),pro(10,000). - Operations use your highest-priority eligible provider key and rotate according to provider priority. Add your own provider API keys in the Enflow dashboard to raise your limits.
Code examples
cURL
# Register
curl -X POST https://enflowpy.vercel.app/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"you@company.com","password":"sup3r-secret","organization_name":"Acme"}'
# Enrich a company
curl -X POST https://enflowpy.vercel.app/api/v1/companies/enrich \
-H "Content-Type: application/json" \
-H "Api-Key: ENFLOW_your_api_key_here" \
-d '{"domain":"usaa.com"}'
# Enrich a person
curl -X POST https://enflowpy.vercel.app/api/v1/people/enrich \
-H "Content-Type: application/json" \
-H "Api-Key: ENFLOW_your_api_key_here" \
-d '{"linkedin_url":"https://www.linkedin.com/in/jane-doe"}'
Python (requests)
import requests
BASE = "https://enflowpy.vercel.app/api/v1"
HEADERS = {"Api-Key": "ENFLOW_your_api_key_here", "Content-Type": "application/json"}
# Register (returns your API key)
r = requests.post(f"{BASE}/auth/register", json={
"email": "you@company.com",
"password": "sup3r-secret",
"organization_name": "Acme",
})
print(r.json()["api_key"]) # ⚠️ save this — shown only once
# Enrich a company
r = requests.post(f"{BASE}/companies/enrich", json={"domain": "usaa.com"}, headers=HEADERS)
data = r.json()
company = data["results"][0]["result"]
print(company["name"], company["industry"], company["employee_count"])
# Enrich people in bulk
r = requests.post(f"{BASE}/people/enrich", json={
"people": [
{"linkedinUrl": "https://www.linkedin.com/in/jane-doe"},
{"email": "john@acme.com"},
]
}, headers=HEADERS)
for row in r.json()["results"]:
print(row["success"], row.get("result", {}).get("email"), row.get("error"))
JavaScript / Node.js
const BASE = "https://enflowpy.vercel.app/api/v1";
async function enrichCompany(domain, apiKey) {
const res = await fetch(`${BASE}/companies/enrich`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Api-Key": apiKey,
},
body: JSON.stringify({ domain }),
});
const json = await res.json();
return json.results?.[0]?.result ?? json;
}
enrichCompany("usaa.com", "ENFLOW_your_api_key_here").then(console.log);
Rate limiting & fair use
- Respect your tier's monthly credits and provider daily quotas (visible in the dashboard).
- Bulk requests above the sync caps (50 companies / 100 people / 20 website-social / 10 website-people) are processed as background jobs — poll
GET /dashboard/jobs/{id}/statusfor completion.
Need support? Contact the Enflow team at the address shown on the Enflow website.