Help
Changelog
Client API version history.
Version numbers match info.version on this API.
Breaking changes increment the major version. Additive endpoints and fields increment minor. Patch is reserved for documentation-only or non-contract fixes.
1.31.0
Limit how many new leads a campaign starts each day
Added
max_new_per_dayon campaigns: how many people who have not been emailed yet the campaign may start each day, by calendar day in its time zone.nullmeans no limit and0means no new leads. Follow-ups are not limited, and unused room does not carry over.- Set it with
createCampaignandupdateCampaign, and sendnullto remove it. Any value other thannullor a whole number from 0 to 10,000 returns 400validation_error.
Changed
- New leads go out in the order they were added to the campaign, for example the row order of an imported file. Before, leads added together went out in no set order.
1.30.0
Choose how often mailboxes send follow-ups before new leads
Added
follow_up_shareon campaigns: how often a mailbox sends a due follow-up instead of a new lead, in percent.100means follow-ups first,0new leads first.- Set it with
createCampaignandupdateCampaign. It is a whole number from 0 to 100 in steps of 10, and new campaigns start at50. Any other value returns 400validation_error.
1.29.0
Which email got the reply
Added
reply.receivedandreply.categorizedincludestep_number,step_name,node_idandflow_node_idfor the sequence email the reply answers.step_numbercounts the main-sequence emails sent to the lead up to and including that email: waits, the categorizer, emails after the categorizer and inbox replies don't count, and each email counts once. A reply to an older email gets that email's step. A reply to an email after the categorizer or to an inbox reply gets the last sequence email sent before it.email.sentincludesstep_name.
Changed
email.sentstep_numbernow uses the same count and is sent for every campaign email. It was missing on most sends and used the email's position in the flow drawing.node_idandflow_node_idstay the email that went out; on an email after the categorizer,step_numberandstep_nameare the last sequence email the lead got.
1.28.0
Likely category on threads
Added
- Every thread has
likely_category: the AI's suggested category (Interested,NeutralorNot Interested) while the thread has nocategoryof its own. The inbox shows the same value as "Likely interested". It is null whencategoryis set or there is no suggestion.listThreads,getThread,updateThreadand the out-of-office endpoints return it. listThreadstakeslikely_categoryto list only the AI's suggestions nobody has confirmed, for examplelikely_category=Interested. It cannot be combined withcategory(400conflicting_filters).listThreadstakesinclude_likely=true. With it,categoryalso matches threads whoselikely_categoryis that category, andno_categoryleaves them out, the same as the inbox's category filters. Without it,categoryworks as before.
1.27.1
Documentation: cc on patchCampaignFlowNode
Changed
patchCampaignFlowNodedocuments the fields it merges into a node, including an email'scc. Patching{ "cc": [...] }works since 1.27.0; this only describes it.
1.27.0
CC on emails after the categorizer
Added
- Email nodes take an optional
cc: up to 5 fixed addresses copied on every send, for every variant. It works on emails downstream of a categorizer (for example the reply to an Interested lead) and is dropped from other emails on save.getCampaignFlowreturns it;saveCampaignFlow,updateCampaignFlowandpatchCampaignFlowNodeaccept it.
Changed
- An invalid
ccaddress (invalid_cc) or more than 5 (too_many_cc) blocks a flow save in every phase, including draft. Other validation codes still come back as draft warnings.
1.26.0
Enrich people
Added
enrichPerson(POST /v1/people/{globalLeadId}/enrich) looks up a person's profile and mobile phone from their email or LinkedIn URL. A new match costs 1 enrichment credit; no match and failed lookups are free. A usable result from the last 30 days comes back free withreused: trueunless you sendrefresh: true. Out of credits returns 402insufficient_credits. HonorsIdempotency-Key.getPersonEnrichment(GET /v1/people/{globalLeadId}/enrichment) returns the newest enrichment, free. Use it to pick up a mobile phone whenenrichPersonreturnedstatus: pending.updatePersonacceptswebsite,linkedin_url,company_linkedin_url,phone_number,mobile_phone_numberandtitle.titleis stored incustom_lead_dataand other custom fields are kept.getPersonmemberships includewebsite,linkedin_url,company_linkedin_url,phone_number,mobile_phone_numberandtitle.
Changed
updatePersontrims values and stores empty strings as null. A person who isn't in any campaign gets the update on their newest lead row instead of none.
1.25.0
Email copy keeps its line breaks
Changed
- Plain-text email copy is literal: each newline in
templateis one line break, so\n\ngives one blank line between paragraphs. Before, each blank line sent as two. templateandbody_textare now always derived frombody_html, one line per paragraph, with entities decoded. Before, they could keep extra newlines or collapse to one line.- On
updateCampaignFlowandpatchCampaignFlowNode, the field you changed wins. If you send a variant with an editedtemplateand the samebody_htmlyou read, the HTML is rebuilt from the text. Before,body_htmlalways won and the text edit was dropped. - The text/plain part of sent emails keeps line breaks.
1.24.0
Import leads from Excel files
Added
importLeadsFromUploadaccepts Excel files (.xlsx, .xls, .ods) as well as CSV. Upload them the same way, with the defaulttext/csvcontent type. A workbook must have exactly one visible sheet with data.
1.23.0
See whose side a bounce is on
Added
bounce_causeon thebounce.detectedwebhook:bad_address(the lead's address doesn't exist or can't receive mail),rejected_our_sender(the receiving server refused mail from the sending IP, domain or mailbox, so the lead may be fine) orunclear.status_codeon thebounce.detectedwebhook: the detailed status code, such as5.1.1or5.7.1, or null.
Changed
- Since 2026-10-08, only a hard bounce with
bounce_cause: bad_addressadds the lead to the block list and emitsblocklist.entry_added, unless the account turned off bounce suppression. A bounce where the receiving server refused the sender no longer block-lists the lead.
1.22.0
Download and send attachments
Added
attachmentson every message fromlistThreadMessages:index,filename,content_type,size,downloadableandnot_downloadable_reason.downloadMessageAttachment(GET /v1/threads/{id}/messages/{message_id}/attachments/{index}) returns a 15-minute signed download URL for a sent or received attachment. Addinclude_content=trueto also getcontent_base64for files of 3 MB or less.createThreadAttachment(POST /v1/threads/{id}/attachments) creates an attachment for a reply or forward, either inline withcontent_base64(4 MB or less) or by PUT to a signedupload_url.attachment_idsoncreateReplyJobandcreateForwardJob. Up to 10 files, 10 MB each, 15 MB total.attachmentson thereply.receivedwebhook, in the same shape as on messages.
Changed
attachmentsonlistThreadMessagesused to return internal storage keys (contentType,part,imapUid,storagePath). It now returns the documented shape above. Readcontent_typeinstead ofcontentType.listThreadMessagesno longer returns the internalsearch_vectorcolumn.
1.21.0
Choose which email providers a campaign and a mailbox send to
Added
recipient_espson campaigns and mailboxes: the email providers (google,microsoft,other) they send first emails to. Set it withcreateCampaign,updateCampaignandupdateMailbox. All three andnullboth mean every provider. Responses always return the explicit list.auto_esp_matchon campaigns. When true, each mailbox prefers leads on its own provider. It is best effort and never delays sending. Therecipient_espsfilters always win over matching.sending_espon mailboxes: the provider the mailbox sends through. Read-only.warningsnext todataoncreateCampaign,updateCampaignandupdateMailbox. Each warning hascode,severityandmessage. They say when a filter leaves leads without a first email, when one provider can only go out from a few mailboxes, when removing a mailbox leaves follow-ups waiting, and which campaigns a mailbox filter affects. The list is empty when there is nothing to warn about.- Filters and matching apply only to a lead's first email. Follow-ups and replies always use the mailbox that sent the first email. When a filter applies, a lead whose provider has not been checked yet waits for the check, usually about a minute.
Changed
updateMailboxacceptsrecipient_espson its own, without a tag field.
1.20.0
Campaigns say when they are finished
Added
display_statusandcompleted_aton campaigns. A running campaign whose leads have all finished the main sequence (reached the end, waiting at the categorizer for a reply, or stopped or paused) shows ascompleted.statusstaysrunning, so reply follow-ups still send and adding leads resumes sending right away. The check runs every few minutes; the campaign reopens as soon as a lead re-enters the main sequence.display_statusfilter onGET /v1/campaigns(draft,scheduled,running,completed,paused,stopped).runningthere excludes completed campaigns;status=runningstill includes them.- Webhooks
campaign.completedandcampaign.reopened. They are off until you enable them for your account or campaign.
1.19.1
Changed
upload_urlfromPOST /v1/uploads/presignno longer carries a fixedx-amz-checksum-crc32(the checksum of an empty file). Upload the same way as before:PUTwith the matchingContent-Type.
1.19.0
Verification runs say why they could not check an address
Added
failedon the verification run result: addresses the verifier could not check this run. They are part ofunchecked.
Changed
- If the verifier rejects the whole account (bad API key, blocked IP, or out of provider credits), the run stops and the job ends
failedwitherrors[0].codeverifier_unavailableand a message saying which. Before, the job endedcompletedwith every address unchecked. Credits reserved for the run are refunded. Lead imports that asked for verification fail the same way. - Finished jobs now always have
completed_atset.
1.18.0
Import a lead CSV from an upload
Added
POST /v1/uploads/{id}/import(importLeadsFromUpload) imports the CSV uploaded to acreateBulkUploadUrlURL. It maps headers to lead fields, turns other columns into custom fields, drops invalid, repeated, and block-listed rows, and reports them ininput.file_reporton the job.
Changed
POST /v1/uploads/presignnow works in every environment, and its description gives the full upload recipe.enrollPeople(POST /v1/campaigns/{id}/enroll) documents only what it accepts: people already in Furnace, byscope,list_id, orglobal_lead_ids. Sendingleadsreturns400 leads_not_supportednamingqueueAsyncLeadImportandimportLeadsFromUpload, instead ofmissing_global_lead_ids.
1.17.0
Optional email verification
Added
POST /v1/email-verification/previewreturns fresh-result reuse, candidates, and remaining verifier credits.verification_optionson lead import and enrollment jobs.verifyis optional.allow_catch_allandallow_unverified_remainderare explicit.POST /v1/email-verification/runschecks addresses and returns counts. It does not enroll leads and does not acceptallow_catch_all.- Import and enroll still accept
allow_catch_allafter that run. Sending it while any address still needs a live check returns409 catch_all_after_verification. - Verification runs accept
Idempotency-Keyand returnestimated_secondsandpoll_after_seconds. - A staged import can pass
import_job_idand finalize withverification_options. - Webhook
email.verification.completedfires when a run finishes. The payload has counts and credits, and no email addresses.
Changed
- A verification run holds at most 20,000 addresses. Over that, the API returns
400 too_many_emails. - Import and enroll counts:
enrolledincludes an address already in this campaign,updatedis how many of those were refreshed,createdis a new lead row, andimportedis the same number asenrolled. - Import and enroll results include
excluded. On a sync bulk import,imported,excluded,skipped, andfailedadd up to the request. The sync add webhook includesexcludedtoo. - A second post of the same sync bulk body returns HTTP 200 and keeps the first
lead.bulk_import.completedwebhook.
1.16.0
Deferral count on message jobs
Added
deferral_countonMessageJob(GET /v1/message-jobs/{id}and the cancel / send-now responses). Counts how many times this intended email was postponed. The job keeps one row across those postponements.
Changed
- Existing keys are unchanged. Jobs that have never been postponed return
0.
1.15.0
CRM-ready webhook identity on every lead email event
Added
- Shared lead identity block on
email.sent,reply.received,reply.categorized, andbounce.detected:email,mailbox_email,campaign_name, contact fields, and nestedcustom_fields email.sentbody_text(plain text, 16,000 character cap) plusstep_number/node_id/flow_node_idwhen knownbounce.detectedexplicitemailandreason(severity+ SMTPcode)- Block list group:
blocklist.entry_addedandblocklist.entry_removedfor everyblock_listinsert or delete (inbox, API, reply opt-out, import, bounce suppression). Email rows may include the lead identity block. Domain rows send the host only (example.com) with noemailor identity fields. A hard bounce may emit bothbounce.detectedandblocklist.entry_added custom_fields_truncated: truewhen custom fields exceed the 8 KB byte budget
Changed
- Existing event keys are unchanged. Empty contact fields are omitted rather than sent as empty strings.
1.14.0
Campaign start and pause dates
Added
lifecycle_scheduleon campaign create/update/detail:time_zone, nullablestart_on/pause_oncalendar dates, and read-only derivedstart_at/pause_at- Campaign status
scheduledfor launches with a futurestart_on - Launch returns
runningorscheduledplus the savedlifecycle_schedule
Changed
- Empty
start_onstill launches immediately. Emptypause_onnever auto-pauses.pause_onis exclusive: sending stops before that local day.
1.13.0
Webhook identity fields for CRM sync
Added
email.sentdatanow includesemail(lead recipient),mailbox_email, andcampaign_nameso CRM integrations can match contacts without a Furnace API keyreply.receiveddatanow includesbody_text(plain-text display body, quoted thread stripped, truncated at 16,000 characters), plusmailbox_emailandcampaign_name
Changed
- Existing
email.sent/reply.receivedkeys are unchanged. Omitted new fields are not sent as empty strings;campaign_namemay benullif the campaign row has no name.
1.12.0
Lead tags and email verification on import
Added
- Optional
tags(array of names/aliases) andemail_verificationonLeadCreate— create, bulk, async, and staged import all inherit the same fields - Person-level lead tags (catalog + account-owned) resolved by name; unknown names find-or-create an account tag
- Structured verification facts (
ok/catch_all/invalid/unknown/disposable) stored separately from tags
Changed
- Omitting the new fields behaves exactly as 1.11.0.
additionalProperties: falsestill rejects unknown keys such asmv_result.
1.11.0
Bulk-first MCP / Client API
Added
GET /v1/meta/limits— page sizes, sync/async caps, queued vs running job quotas, supported scopes/operations, file-ingress capabilitiesPOST /v1/bulk/preview— estimate matched/excluded/actionable counts; bind execution withpreview_id- Staged lead import:
POST /v1/campaigns/{id}/imports/staged,POST /v1/jobs/{id}/staging-rows,POST /v1/jobs/{id}/finalize - Optional
POST /v1/uploads/presignfor direct S3 CSV upload (upload_id); local filesystem paths are never accepted POST /v1/people/export(exportPeople) and compact email/global_lead_idprojectionPOST /v1/campaigns/{id}/enroll(enrollPeople) with server-sidescope+exclusionsPOST /v1/lead-lists/{id}/members:bulk(updateLeadListMembership) async add/removePOST /v1/jobs/{id}/cancel— cancel queued/uploading immediately; stop running jobs between chunks- Job operations:
add_to_lead_list,remove_from_lead_list,export_leads,csv_lead_import_staged - Campaign detail now returns attached
mailbox_ids
Changed
- Async job capacity: only running jobs consume the concurrent slot (default 3); additional jobs stay queued up to a separate quota
createAsyncJobacceptsscope,exclusions,preview_id,target_list_id,source_campaign_id, and export projection fields- Lead-source
bucketIdis normalized to the campaign bucket on create/save; richText variants derivebody_htmlfromtemplate/body_textwhen empty
1.10.0
Campaign create defaults to Central business hours
Changed
POST /v1/campaignsnow defaults omittedscheduleto Central 9–5 Mon–Fri (America/Chicago). Pass"schedule": nullfor 24/7.- Omitted
sending_interval_secondsdefaults to1440(24 minutes; ~20 emails per mailbox per day on the default window), replacing the previous300default.
1.9.0
Replace lead preview + structured errors
Added
GET /v1/threads/{id}/replace-lead/preview?email=— read-only preview of create vs attach, block-list status, match count, and whether the write would be refused (allowed/disallowed_reason)- MCP tool
previewThreadLeadReplacement(auto-generated from the new endpoint) POST /v1/threads/{id}/replace-leadacceptsnew_mobile_phone_number- The same response returns
target_lead_id: the pre-existing contact onmode = attached, otherwisenull
Changed
- Replace-lead business-rule failures now return structured 400/403/404/409 instead of opaque 500s (
same_as_current_lead,lead_already_replaced,target_missing_enrollment,target_already_replaced,lead_not_found,invalid_reason, …) replaceThreadLeaddescription documents create vs attach side effects and points agents at the preview tool
1.8.0
Replace lead reuses an existing campaign contact
Added
POST /v1/threads/{id}/replace-leadreturnsmode:attachedwhen the replacement address was already a live lead in the campaign,createdotherwise- The same response returns
retired_sibling_count, how many duplicate rows of that address had their sequence stopped
Changed
- Replacing to an address that is already in the campaign no longer creates a second lead. The existing contact is reused, the conversation moves to them, and the replaced lead is retired as stopped/replaced instead of archived.
new_lead_idis that pre-existing contact. - The call now fails if the existing contact has no enrollment in the campaign, since the forward would have nothing to send against
1.7.0
Account settings + MCP surface
Added
GET/PUT /v1/webhooks— account webhook URL, signing secret, enabled eventsGET/POST /v1/api-keys,DELETE /v1/api-keys/{id}— create returns secret once; list omits secretPOST/GET /v1/mailboxes/connect-sessions— start mailbox connect handoff and poll status
1.6.1
People response shape + request body hygiene
Changed
GET /v1/peopleand lead-list people pages now return the same person fields as detail/PATCH, withlatest_activity_at(list no longer emitslatest_activityor per-rowtotal_count)PATCH /v1/campaigns/{id}accepts"schedule": nullto clear the send window (24/7)- Unknown keys on primary request bodies are ignored (stripped) so handlers match closed OpenAPI schemas
1.6.0
Inbox triage timestamps
Added
- Thread responses include
last_inbound_at— latest inbound lead reply timestamp
Changed
GET /v1/threadsdate_from/date_toand Newest/Oldest sort now uselast_inbound_at(lead reply time), notlast_message_atlast_message_atremains latest activity in either direction
1.5.0
Inbox thread search
Changed
GET /v1/threads?q=— free-text search now matches subject, participants, lead name/email/company, campaign name, thread tags, and message bodies (prefix/FTS; minimum 2 characters)
1.4.3
Self-hosted docs rebuild (Fumadocs + OpenAPI reference + agent layer)
Changed
- Replaced Starlight/Scalar with a unified Fumadocs site at
/docs - API reference at
/docs/reference/uses fumadocs-openapi inside the Furnace docs shell (read-only, no try-it console) - Documentation and API Reference are separated in the header (Mintlify-style tabs)
- Split building campaigns into quickstart, flow, launch, and flow-schemas guides
- Auto-generated
llms.txt,llms-full.txt, and per-page.mdmirrors for agent access
1.4.2
Starlight documentation site
Changed
- Replaced Scalar at
/docswith a Starlight static docs site (guides + OpenAPI reference via starlight-openapi) - Removed phantom
/documentation/*OpenAPI paths; guides export from TS builders at build time - Building campaigns guide now documents
POST /flowas the hero save (PUT remains a deprecated alias)
1.4.1
Documentation consolidation
Changed
- Removed the Campaign flow reference guide page — field-level flow object docs now live in Models (
CampaignFlow,FlowUpdate,FlowValidationIssue, and related node schemas) - Building campaigns guide and flow API endpoint descriptions now link to Models schemas
1.4.0
Campaign build lifecycle & flow pipeline
Added
- Building campaigns guide in
/docs(Guide → Building campaigns) with lifecycle rules, copy-pasteable flow JSON, and draft-vs-live locking behavior - Campaign flow reference guide (
/documentation/campaign-flow-reference) — field-by-field flow object reference, merge variables, normalization rules, and full validation error-code catalog POST /v1/campaigns— create a draft campaign with optional mailboxes, tags, schedule, and initial flowPOST /v1/campaigns/{id}/flow— hero flow save withflow_revision,field_sync, and optionalIf-MatchconcurrencyPOST /v1/campaigns/{id}/flow?dry_run=true— dry-run alias for flow validation without persistingPUT /v1/campaigns/{id}/flow— write the canonical campaign flow payload (deprecated alias ofPOST)POST /v1/campaigns/{id}/flow:validate— dry-run normalization, validation, and lifecycle gatingPATCH /v1/campaigns/{id}/status— pause, resume, or stop live campaigns (running|paused|stopped)PATCH /v1/campaigns/{id}/flow/nodes/{nodeId}— live content-only node patchGET /v1/flow-templates— starter flow graphsGET /v1/campaigns/{id}?include=launch_state,lead_field_state— checklist observability without extra validate callsPOST /v1/campaigns/{id}/launch— start a draft campaign after backfilling enrollmentsfield_syncon flow saves — auto-declares merge-variable fields from email copy
Changed
GET /v1/campaignslist responses omitflow_data; useGET /v1/campaigns/{id}for the full flow- Campaign detail responses include computed
flow_revision POST /v1/campaigns/{id}/launchreturns{ enrolled: N }and uses shared launch validation- Live campaign flows are now topology-locked in both the API and the builder UI. Structural edits return
403 permission_errorwith codeflow_locked. POST /v1/campaigns/{id}/lead-fieldsnow writes flow data through the same service-role-safe persistence path as the new flow endpoints.- Building campaigns guide expanded with end-to-end curl walkthrough,
flow:validateresponse examples, structural change reason codes, launch preconditions, and troubleshooting table - OpenAPI schemas for flow node types (
EmailVariant,LeadSourceNodeData,EmailNodeData,WaitTimeNodeData,AICategorizerNodeData,DataSenderNodeData,FlowNode,FlowEdge,FlowValidateResult) now include per-field descriptions and examples
1.3.0
Webhooks — categorization and delivery infrastructure
Added
- Outbound webhook
reply.categorizedwhen a thread reply category is assigned, changed, or cleared - Granular per-event webhook subscription in Account Settings (expand event groups to pick individual types)
Changed
- Campaign pause, resume, and stop from the Furnace app now emit
campaign.paused,campaign.resumed, andcampaign.stoppedwebhooks (previously Client API only) PATCH /v1/threads/{id}category updates emitreply.categorized
1.2.0
Inbox expansion — triage, outbound messaging, and ops endpoints for programmatic inbox use.
Added
- Webhooks guide in
/docs(Guide → Webhooks) with example payloads for every outbound event type - Consolidated
/docsinto a single Scalar document with Guide and API sidebar sections
Thread list & triage
GET /v1/threads— new query params:q,unread_only,conversation_status,category(no_categoryfor uncategorized),tag_ids,date_from,date_to,has_reply_only(defaulttrue)PATCH /v1/threads/{id}— partial update:category,conversation_status,read
Outbound messaging
POST /v1/threads/{id}/forward— queue forward job (forward_message_idrequired)GET /v1/message-jobs/{id}— poll reply/forward job statusPOST /v1/message-jobs/{id}/cancel— cancel queued/failed outbound jobPOST /v1/message-jobs/{id}/send-now— expedite queued outbound job
Inbox ops
PUT /v1/threads/{id}/out-of-office— set OOO (resume_mode:scheduled|instant|none)DELETE /v1/threads/{id}/out-of-office— clear OOOPOST /v1/threads/{id}/replace-lead— replace thread lead; optionalforward_message_idGET /v1/thread-tags— list account thread tagsPOST /v1/threads/{id}/tags:add/tags:remove— assign or remove tag on thread
Changed
POST /v1/threads/{id}/reply— optionalin_reply_to_message_id(defaults to latest message)
Notes
- Poll outbound sends with
/v1/message-jobs/{id}, not/v1/jobs/{id}(import jobs only). - Tag create/edit/delete remains in the Furnace app; the API supports list + assign/remove only.
1.1.0
People, lists, jobs, and batch webhooks
Added
GET/PATCH /v1/people,GET/PATCH /v1/people/{globalLeadId}GET/POST/PATCH/DELETE /v1/lead-lists, list membership endpointsPOST /v1/jobs,GET /v1/jobs/{id}— async bulk operations- Campaign/mailbox tag CRUD and filtering
- Sync bulk shortcuts:
leads:add,leads:remove,leads:remove-from-all-campaigns, enrollment pause/resume - Batch completion webhooks (
*.completed) for bulk and enrollment actions
Changed
- Webhook allowlist: removed
enrollment.created/enrollment.updated; added batch completion events
1.0.0
Initial Client API
Added
- Campaigns, leads, mailboxes, block list, campaign stats
- Basic inbox:
GET /v1/threads,GET /v1/threads/{id},GET /v1/threads/{id}/messages,POST /v1/threads/{id}/reply - Atomic webhooks:
lead.*, campaign lifecycle,email.sent,reply.received,bounce.detected - OpenAPI at
/openapi.json, Scalar UI at/docs