{"openapi":"3.1.0","info":{"title":"Cleanlist API (v1)","description":"Legacy (v1) public API: API-key validation, bulk enrichment + status polling, webhook delivery history, and folder creation.","version":"2.0.0"},"paths":{"/api/v1/public/auth/validate-key":{"get":{"tags":["Public Workspace"],"summary":"Validate an API key","description":"Verify that the supplied API key is valid and return the identity it maps to.\nUse it as a lightweight health check when wiring up a new integration or\nrotating credentials — a 200 confirms the key is active and reveals the bound\nuser and organization, while an invalid or revoked key fails with 401 before\nreaching this handler. Requires a Bearer API key and counts against your\npublic-API rate limit.\n\n**📖 Docs:** https://docs.cleanlist.ai/api-reference/authentication","operationId":"validate_api_key_api_v1_public_auth_validate_key_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateApiKeyResponse"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/public/enrich/bulk":{"post":{"tags":["Public Enrichment"],"summary":"Start a bulk enrichment run","description":"Kick off an asynchronous bulk enrichment run for up to 250 contacts and get\nback a workflow_id plus per-contact task IDs immediately. Enrichment then runs\nin the background across a provider waterfall, so poll GET /enrich/status (by\nworkflow_id or task_id) or supply a webhook_url to receive results as they\nland. Credits are charged per successfully enriched contact by mode — partial\n(1), phone_only (10), full (11) — and results are also saved to your default\n\"Extension Leads\" lead list. Requires a Bearer API key.\n\n**📖 Docs:** https://docs.cleanlist.ai/api-reference/enrichment\n**📖 Docs:** https://docs.cleanlist.ai/api-reference/enrichment-types\n**📖 Docs:** https://docs.cleanlist.ai/guides/bulk-enrichment","operationId":"start_public_bulk_enrichment_api_v1_public_enrich_bulk_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicBulkEnrichmentRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicBulkEnrichmentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}},"/api/v1/public/enrich/status":{"get":{"tags":["Public Enrichment"],"summary":"Poll a bulk enrichment run","description":"Poll the status and results of a bulk enrichment run. Provide exactly one of\nworkflow_id (aggregate run state plus per-contact progress) or task_id (the\nstate and enriched fields of a single contact) — supplying both or neither\nreturns a 400. Use this after POST /enrich/bulk when you are not receiving\nwebhooks; null fields are omitted from the response. Requires a Bearer API key.\n\n**📖 Docs:** https://docs.cleanlist.ai/api-reference/enrichment","operationId":"get_public_enrichment_status_api_v1_public_enrich_status_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"workflow_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Workflow Id"}},{"name":"task_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Task Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicStatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/public/webhooks/deliveries":{"get":{"tags":["Public Webhooks"],"summary":"List webhook deliveries","description":"List webhook delivery attempts for a given workflow_id, newest first, so you\ncan confirm callbacks were sent and debug failures such as timeouts, non-2xx\nresponses, and retries. Each row records the event type, attempt number, HTTP\nstatus, timing, and any error message. Reach for it when a contact appears\nenriched but your endpoint never received the callback. Requires a Bearer API\nkey and returns up to `limit` rows (default 50, max 200).\n\n**📖 Docs:** https://docs.cleanlist.ai/api-reference/webhooks\n**📖 Docs:** https://docs.cleanlist.ai/guides/receiving-webhooks","operationId":"list_public_webhook_deliveries_api_v1_public_webhooks_deliveries_get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"workflow_id","in":"query","required":true,"schema":{"type":"string","title":"Workflow Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicWebhookDeliveryResponse"},"title":"Response List Public Webhook Deliveries Api V1 Public Webhooks Deliveries Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/public/folders":{"post":{"tags":["Public Lead Lists"],"summary":"Create a folder","description":"Create a folder to organize your lead lists, optionally nested under an\nexisting folder you own via parent_id. Use it to group lists by campaign,\nteam, or quarter before importing or enriching contacts. Returns the created\nfolder with its generated UUID and a 201 status; an invalid parent_id yields a\n400 and a non-owned or missing parent yields a 404. Requires a Bearer API key.\n\n**📖 Docs:** https://docs.cleanlist.ai/api-reference/lead-lists","operationId":"public_create_folder_api_v1_public_folders_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicFolderCreate"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicFolderResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"HTTPBearer":[]}]}}},"components":{"schemas":{"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"PublicBulkContact":{"properties":{"linkedin_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Linkedin Url","description":"Public LinkedIn profile URL of the contact. When present this alone is enough to enrich the contact and the name/company fields are optional.","examples":["https://www.linkedin.com/in/jane-doe-9a1b"]},"first_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Name","description":"Contact's first name. Required (together with last_name and a company field) when no linkedin_url is supplied.","examples":["Jane"]},"last_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Name","description":"Contact's last name. Required (together with first_name and a company field) when no linkedin_url is supplied.","examples":["Doe"]},"company_domain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Domain","description":"Company website domain. Paired with first_name + last_name to resolve the person when no linkedin_url is supplied. Preferred over company_name for accuracy.","examples":["acme.io"]},"company_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Company Name","description":"Company name. Alternative to company_domain when pairing with first_name + last_name to identify the contact.","examples":["Acme"]}},"type":"object","title":"PublicBulkContact","example":{"company_domain":"acme.io","company_name":"Acme","first_name":"Jane","last_name":"Doe","linkedin_url":"https://www.linkedin.com/in/jane-doe-9a1b"}},"PublicBulkEnrichmentRequest":{"properties":{"enrichment_type":{"type":"string","enum":["partial","phone_only","full"],"title":"Enrichment Type","description":"Enrichment depth to run for every contact. `partial` returns email + LinkedIn + title + company (1 credit/contact); `phone_only` returns a mobile phone with no email (10 credits/contact); `full` returns email AND phone (11 credits/contact). Only these three modes are supported here — 'prospecting_only' needs a people-search discovery step first and is rejected, because dispatching it with no person identifiers returns a 500.","default":"partial","examples":["full"]},"webhook_url":{"anyOf":[{"type":"string","maxLength":2083,"minLength":1,"format":"uri"},{"type":"null"}],"title":"Webhook Url","description":"Optional HTTPS URL that receives a POST callback for each contact as enrichment completes, so you don't have to poll /enrich/status. Delivery attempts are recorded on GET /webhooks/deliveries.","examples":["https://hooks.acme.io/cleanlist/enrichment"]},"contacts":{"items":{"$ref":"#/components/schemas/PublicBulkContact"},"type":"array","title":"Contacts","description":"Contacts to enrich, up to 250 per request. Each contact must include a linkedin_url OR first_name + last_name + (company_domain or company_name)."}},"type":"object","required":["contacts"],"title":"PublicBulkEnrichmentRequest","example":{"contacts":[{"company_domain":"acme.io","company_name":"Acme","first_name":"Jane","last_name":"Doe","linkedin_url":"https://www.linkedin.com/in/jane-doe-9a1b"},{"company_domain":"globex.com","first_name":"John","last_name":"Smith"}],"enrichment_type":"full","webhook_url":"https://hooks.acme.io/cleanlist/enrichment"}},"PublicBulkEnrichmentResponse":{"properties":{"workflow_id":{"type":"string","title":"Workflow Id","description":"Identifier of the async bulk enrichment run. Pass it to GET /enrich/status to poll progress and to GET /webhooks/deliveries to inspect callbacks.","examples":["wf_bulk_7c2a"]},"status":{"type":"string","title":"Status","description":"Initial workflow status at dispatch — typically 'processing' while contacts are enriched in the background.","examples":["processing"]},"message":{"type":"string","title":"Message","description":"Human-readable summary of what was dispatched.","examples":["Bulk enrichment started for 2 contacts."]},"task_ids":{"items":{"type":"string"},"type":"array","title":"Task Ids","description":"Per-contact task identifiers, one per accepted contact. Each can be polled individually via GET /enrich/status?task_id=...","examples":[["enrich-1f4c9a20","enrich-2b7d3e11"]]},"total_contacts":{"type":"integer","title":"Total Contacts","description":"Number of contacts accepted into the run.","examples":[2]}},"type":"object","required":["workflow_id","status","message","task_ids","total_contacts"],"title":"PublicBulkEnrichmentResponse","example":{"message":"Bulk enrichment started for 2 contacts.","status":"processing","task_ids":["enrich-1f4c9a20","enrich-2b7d3e11"],"total_contacts":2,"workflow_id":"wf_bulk_7c2a"}},"PublicFolderCreate":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name","description":"Display name for the new folder. 1–255 characters.","examples":["Q3 Outbound"]},"parent_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parent Id","description":"Optional UUID of an existing folder you own to nest this folder under. Omit to create a top-level folder. A 404 is returned if the parent doesn't exist or isn't yours; a 400 if it isn't a valid UUID.","examples":["3f9a1c2e-7b64-4d2a-9c11-0a2b3c4d5e6f"]}},"type":"object","required":["name"],"title":"PublicFolderCreate","example":{"name":"Q3 Outbound","parent_id":"3f9a1c2e-7b64-4d2a-9c11-0a2b3c4d5e6f"}},"PublicFolderResponse":{"properties":{"id":{"type":"string","title":"Id","description":"UUID of the created folder.","examples":["b1e7c9d4-2f3a-4c8e-9a1b-6d5e4f3c2b1a"]},"name":{"type":"string","title":"Name","description":"Folder display name (trimmed).","examples":["Q3 Outbound"]},"parent_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parent Id","description":"UUID of the parent folder, or null for a top-level folder.","examples":["3f9a1c2e-7b64-4d2a-9c11-0a2b3c4d5e6f"]},"user_id":{"type":"string","title":"User Id","description":"Clerk user ID that owns the folder.","examples":["user_2abcJANEdoe456"]},"organization_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Organization Id","description":"Clerk organization ID the folder is scoped to.","examples":["org_2acmeIO789xyz"]},"created_at":{"type":"string","format":"date-time","title":"Created At","description":"ISO-8601 timestamp when the folder was created.","examples":["2026-06-30T18:24:05Z"]}},"type":"object","required":["id","name","user_id","created_at"],"title":"PublicFolderResponse","example":{"created_at":"2026-06-30T18:24:05Z","id":"b1e7c9d4-2f3a-4c8e-9a1b-6d5e4f3c2b1a","name":"Q3 Outbound","organization_id":"org_2acmeIO789xyz","parent_id":"3f9a1c2e-7b64-4d2a-9c11-0a2b3c4d5e6f","user_id":"user_2abcJANEdoe456"}},"PublicStatusResponse":{"properties":{"workflow":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Workflow","description":"Present when polling by workflow_id: aggregate state of the whole bulk run — overall status plus per-contact progress counts and results. Omitted when polling by task_id.","examples":[{"completed":2,"failed":0,"results":[{"company_domain":"acme.io","company_name":"Acme","email":"jane.doe@acme.io","first_name":"Jane","last_name":"Doe","linkedin_url":"https://www.linkedin.com/in/jane-doe-9a1b","phone":"+14155550123","status":"completed","task_id":"enrich-1f4c9a20","title":"VP of Sales"}],"status":"completed","total":2,"workflow_id":"wf_bulk_7c2a"}]},"task":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Task","description":"Present when polling by task_id: the state and enriched fields of a single contact, including credits actually charged. Omitted when polling by workflow_id.","examples":[{"credits_used":11,"email":"jane.doe@acme.io","phone":"+14155550123","status":"completed","task_id":"enrich-1f4c9a20"}]}},"type":"object","title":"PublicStatusResponse","example":{"workflow":{"completed":2,"failed":0,"results":[{"company_domain":"acme.io","company_name":"Acme","email":"jane.doe@acme.io","first_name":"Jane","last_name":"Doe","linkedin_url":"https://www.linkedin.com/in/jane-doe-9a1b","phone":"+14155550123","status":"completed","task_id":"enrich-1f4c9a20","title":"VP of Sales"}],"status":"completed","total":2,"workflow_id":"wf_bulk_7c2a"}}},"PublicWebhookDeliveryResponse":{"properties":{"id":{"type":"string","title":"Id","description":"Unique identifier of this delivery attempt.","examples":["whd_8f21ac"]},"webhook_id":{"type":"string","title":"Webhook Id","description":"Identifier of the webhook subscription that was invoked.","examples":["wh_acme_prod"]},"workflow_id":{"type":"string","title":"Workflow Id","description":"Bulk enrichment run this delivery belongs to.","examples":["wf_bulk_7c2a"]},"event_type":{"type":"string","title":"Event Type","description":"Event that triggered the delivery, e.g. 'enrichment.contact.completed' or 'enrichment.workflow.completed'.","examples":["enrichment.contact.completed"]},"attempt_number":{"type":"integer","title":"Attempt Number","description":"1-based retry attempt; increments each time a failed delivery is retried.","examples":[1]},"status":{"type":"string","title":"Status","description":"Delivery outcome: 'success', 'failed', or 'pending'.","examples":["success"]},"response_status_code":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Response Status Code","description":"HTTP status your endpoint returned, if the request completed.","examples":[200]},"error_message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Message","description":"Error detail when the delivery failed (timeout, connection refused, non-2xx response). Null on success.","examples":["Connection timed out after 5000ms"]},"duration_ms":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Duration Ms","description":"Round-trip time of the delivery request, in milliseconds.","examples":[142]},"created_at":{"type":"string","format":"date-time","title":"Created At","description":"ISO-8601 timestamp when the delivery attempt was made.","examples":["2026-06-30T18:24:05Z"]}},"type":"object","required":["id","webhook_id","workflow_id","event_type","attempt_number","status","created_at"],"title":"PublicWebhookDeliveryResponse","example":{"attempt_number":1,"created_at":"2026-06-30T18:24:05Z","duration_ms":142,"event_type":"enrichment.contact.completed","id":"whd_8f21ac","response_status_code":200,"status":"success","webhook_id":"wh_acme_prod","workflow_id":"wf_bulk_7c2a"}},"ValidateApiKeyResponse":{"properties":{"valid":{"type":"boolean","title":"Valid","description":"Whether the presented API key is valid and active. Always `true` on a 200 response — invalid or revoked keys fail with 401 during authentication and never reach this body.","examples":[true]},"user_id":{"type":"string","title":"User Id","description":"Clerk user ID the API key is bound to.","examples":["user_2abcJANEdoe456"]},"organization_id":{"type":"string","title":"Organization Id","description":"Clerk organization ID the key belongs to. All credit usage, lead lists, and rate limits are scoped to this organization.","examples":["org_2acmeIO789xyz"]}},"type":"object","required":["valid","user_id","organization_id"],"title":"ValidateApiKeyResponse","example":{"organization_id":"org_2acmeIO789xyz","user_id":"user_2abcJANEdoe456","valid":true}},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}},"securitySchemes":{"HTTPBearer":{"type":"http","scheme":"bearer"}}},"tags":[{"name":"Public Workspace","description":"Workspace, credits, usage"},{"name":"Public Lead Lists","description":"Lead list management"},{"name":"Public Enrichment","description":"Enrichment operations"},{"name":"Public Webhooks","description":"Webhook delivery history"}],"servers":[{"url":"https://api.cleanlist.ai","description":"Production"},{"url":"http://localhost:8000","description":"Local development"}]}