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_day on campaigns: how many people who have not been emailed yet the campaign may start each day, by calendar day in its time zone. null means no limit and 0 means no new leads. Follow-ups are not limited, and unused room does not carry over.
  • Set it with createCampaign and updateCampaign, and send null to remove it. Any value other than null or a whole number from 0 to 10,000 returns 400 validation_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_share on campaigns: how often a mailbox sends a due follow-up instead of a new lead, in percent. 100 means follow-ups first, 0 new leads first.
  • Set it with createCampaign and updateCampaign. It is a whole number from 0 to 100 in steps of 10, and new campaigns start at 50. Any other value returns 400 validation_error.

1.29.0

Which email got the reply

Added

  • reply.received and reply.categorized include step_number, step_name, node_id and flow_node_id for the sequence email the reply answers. step_number counts 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.sent includes step_name.

Changed

  • email.sent step_number now 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_id and flow_node_id stay the email that went out; on an email after the categorizer, step_number and step_name are 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, Neutral or Not Interested) while the thread has no category of its own. The inbox shows the same value as "Likely interested". It is null when category is set or there is no suggestion. listThreads, getThread, updateThread and the out-of-office endpoints return it.
  • listThreads takes likely_category to list only the AI's suggestions nobody has confirmed, for example likely_category=Interested. It cannot be combined with category (400 conflicting_filters).
  • listThreads takes include_likely=true. With it, category also matches threads whose likely_category is that category, and no_category leaves them out, the same as the inbox's category filters. Without it, category works as before.

1.27.1

Documentation: cc on patchCampaignFlowNode

Changed

  • patchCampaignFlowNode documents the fields it merges into a node, including an email's cc. 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. getCampaignFlow returns it; saveCampaignFlow, updateCampaignFlow and patchCampaignFlowNode accept it.

Changed

  • An invalid cc address (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 with reused: true unless you send refresh: true. Out of credits returns 402 insufficient_credits. Honors Idempotency-Key.
  • getPersonEnrichment (GET /v1/people/{globalLeadId}/enrichment) returns the newest enrichment, free. Use it to pick up a mobile phone when enrichPerson returned status: pending.
  • updatePerson accepts website, linkedin_url, company_linkedin_url, phone_number, mobile_phone_number and title. title is stored in custom_lead_data and other custom fields are kept.
  • getPerson memberships include website, linkedin_url, company_linkedin_url, phone_number, mobile_phone_number and title.

Changed

  • updatePerson trims 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 template is one line break, so \n\n gives one blank line between paragraphs. Before, each blank line sent as two.
  • template and body_text are now always derived from body_html, one line per paragraph, with entities decoded. Before, they could keep extra newlines or collapse to one line.
  • On updateCampaignFlow and patchCampaignFlowNode, the field you changed wins. If you send a variant with an edited template and the same body_html you read, the HTML is rebuilt from the text. Before, body_html always 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

  • importLeadsFromUpload accepts Excel files (.xlsx, .xls, .ods) as well as CSV. Upload them the same way, with the default text/csv content type. A workbook must have exactly one visible sheet with data.

1.23.0

See whose side a bounce is on

Added

  • bounce_cause on the bounce.detected webhook: 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) or unclear.
  • status_code on the bounce.detected webhook: the detailed status code, such as 5.1.1 or 5.7.1, or null.

Changed

  • Since 2026-10-08, only a hard bounce with bounce_cause: bad_address adds the lead to the block list and emits blocklist.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

  • attachments on every message from listThreadMessages: index, filename, content_type, size, downloadable and not_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. Add include_content=true to also get content_base64 for files of 3 MB or less.
  • createThreadAttachment (POST /v1/threads/{id}/attachments) creates an attachment for a reply or forward, either inline with content_base64 (4 MB or less) or by PUT to a signed upload_url.
  • attachment_ids on createReplyJob and createForwardJob. Up to 10 files, 10 MB each, 15 MB total.
  • attachments on the reply.received webhook, in the same shape as on messages.

Changed

  • attachments on listThreadMessages used to return internal storage keys (contentType, part, imapUid, storagePath). It now returns the documented shape above. Read content_type instead of contentType.
  • listThreadMessages no longer returns the internal search_vector column.

1.21.0

Choose which email providers a campaign and a mailbox send to

Added

  • recipient_esps on campaigns and mailboxes: the email providers (google, microsoft, other) they send first emails to. Set it with createCampaign, updateCampaign and updateMailbox. All three and null both mean every provider. Responses always return the explicit list.
  • auto_esp_match on campaigns. When true, each mailbox prefers leads on its own provider. It is best effort and never delays sending. The recipient_esps filters always win over matching.
  • sending_esp on mailboxes: the provider the mailbox sends through. Read-only.
  • warnings next to data on createCampaign, updateCampaign and updateMailbox. Each warning has code, severity and message. 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

  • updateMailbox accepts recipient_esps on its own, without a tag field.

1.20.0

Campaigns say when they are finished

Added

  • display_status and completed_at on 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 as completed. status stays running, 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_status filter on GET /v1/campaigns (draft, scheduled, running, completed, paused, stopped). running there excludes completed campaigns; status=running still includes them.
  • Webhooks campaign.completed and campaign.reopened. They are off until you enable them for your account or campaign.

1.19.1

Changed

  • upload_url from POST /v1/uploads/presign no longer carries a fixed x-amz-checksum-crc32 (the checksum of an empty file). Upload the same way as before: PUT with the matching Content-Type.

1.19.0

Verification runs say why they could not check an address

Added

  • failed on the verification run result: addresses the verifier could not check this run. They are part of unchecked.

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 failed with errors[0].code verifier_unavailable and a message saying which. Before, the job ended completed with 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_at set.

1.18.0

Import a lead CSV from an upload

Added

  • POST /v1/uploads/{id}/import (importLeadsFromUpload) imports the CSV uploaded to a createBulkUploadUrl URL. It maps headers to lead fields, turns other columns into custom fields, drops invalid, repeated, and block-listed rows, and reports them in input.file_report on the job.

Changed

  • POST /v1/uploads/presign now 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, by scope, list_id, or global_lead_ids. Sending leads returns 400 leads_not_supported naming queueAsyncLeadImport and importLeadsFromUpload, instead of missing_global_lead_ids.

1.17.0

Optional email verification

Added

  • POST /v1/email-verification/preview returns fresh-result reuse, candidates, and remaining verifier credits.
  • verification_options on lead import and enrollment jobs. verify is optional. allow_catch_all and allow_unverified_remainder are explicit.
  • POST /v1/email-verification/runs checks addresses and returns counts. It does not enroll leads and does not accept allow_catch_all.
  • Import and enroll still accept allow_catch_all after that run. Sending it while any address still needs a live check returns 409 catch_all_after_verification.
  • Verification runs accept Idempotency-Key and return estimated_seconds and poll_after_seconds.
  • A staged import can pass import_job_id and finalize with verification_options.
  • Webhook email.verification.completed fires 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: enrolled includes an address already in this campaign, updated is how many of those were refreshed, created is a new lead row, and imported is the same number as enrolled.
  • Import and enroll results include excluded. On a sync bulk import, imported, excluded, skipped, and failed add up to the request. The sync add webhook includes excluded too.
  • A second post of the same sync bulk body returns HTTP 200 and keeps the first lead.bulk_import.completed webhook.

1.16.0

Deferral count on message jobs

Added

  • deferral_count on MessageJob (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, and bounce.detected: email, mailbox_email, campaign_name, contact fields, and nested custom_fields
  • email.sent body_text (plain text, 16,000 character cap) plus step_number / node_id / flow_node_id when known
  • bounce.detected explicit email and reason (severity + SMTP code)
  • Block list group: blocklist.entry_added and blocklist.entry_removed for every block_list insert 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 no email or identity fields. A hard bounce may emit both bounce.detected and blocklist.entry_added
  • custom_fields_truncated: true when 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_schedule on campaign create/update/detail: time_zone, nullable start_on/pause_on calendar dates, and read-only derived start_at/pause_at
  • Campaign status scheduled for launches with a future start_on
  • Launch returns running or scheduled plus the saved lifecycle_schedule

Changed

  • Empty start_on still launches immediately. Empty pause_on never auto-pauses. pause_on is exclusive: sending stops before that local day.

1.13.0

Webhook identity fields for CRM sync

Added

  • email.sent data now includes email (lead recipient), mailbox_email, and campaign_name so CRM integrations can match contacts without a Furnace API key
  • reply.received data now includes body_text (plain-text display body, quoted thread stripped, truncated at 16,000 characters), plus mailbox_email and campaign_name

Changed

  • Existing email.sent / reply.received keys are unchanged. Omitted new fields are not sent as empty strings; campaign_name may be null if the campaign row has no name.

1.12.0

Lead tags and email verification on import

Added

  • Optional tags (array of names/aliases) and email_verification on LeadCreate — 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: false still rejects unknown keys such as mv_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 capabilities
  • POST /v1/bulk/preview — estimate matched/excluded/actionable counts; bind execution with preview_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/presign for direct S3 CSV upload (upload_id); local filesystem paths are never accepted
  • POST /v1/people/export (exportPeople) and compact email/global_lead_id projection
  • POST /v1/campaigns/{id}/enroll (enrollPeople) with server-side scope + exclusions
  • POST /v1/lead-lists/{id}/members:bulk (updateLeadListMembership) async add/remove
  • POST /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
  • createAsyncJob accepts scope, exclusions, preview_id, target_list_id, source_campaign_id, and export projection fields
  • Lead-source bucketId is normalized to the campaign bucket on create/save; richText variants derive body_html from template/body_text when empty

1.10.0

Campaign create defaults to Central business hours

Changed

  • POST /v1/campaigns now defaults omitted schedule to Central 9–5 Mon–Fri (America/Chicago). Pass "schedule": null for 24/7.
  • Omitted sending_interval_seconds defaults to 1440 (24 minutes; ~20 emails per mailbox per day on the default window), replacing the previous 300 default.

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-lead accepts new_mobile_phone_number
  • The same response returns target_lead_id: the pre-existing contact on mode = attached, otherwise null

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, …)
  • replaceThreadLead description 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-lead returns mode: attached when the replacement address was already a live lead in the campaign, created otherwise
  • 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_id is 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 events
  • GET/POST /v1/api-keys, DELETE /v1/api-keys/{id} — create returns secret once; list omits secret
  • POST/GET /v1/mailboxes/connect-sessions — start mailbox connect handoff and poll status

1.6.1

People response shape + request body hygiene

Changed

  • GET /v1/people and lead-list people pages now return the same person fields as detail/PATCH, with latest_activity_at (list no longer emits latest_activity or per-row total_count)
  • PATCH /v1/campaigns/{id} accepts "schedule": null to 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/threads date_from / date_to and Newest/Oldest sort now use last_inbound_at (lead reply time), not last_message_at
  • last_message_at remains 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 .md mirrors for agent access

1.4.2

Starlight documentation site

Changed

  • Replaced Scalar at /docs with 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 /flow as 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 flow
  • POST /v1/campaigns/{id}/flow — hero flow save with flow_revision, field_sync, and optional If-Match concurrency
  • POST /v1/campaigns/{id}/flow?dry_run=true — dry-run alias for flow validation without persisting
  • PUT /v1/campaigns/{id}/flow — write the canonical campaign flow payload (deprecated alias of POST)
  • POST /v1/campaigns/{id}/flow:validate — dry-run normalization, validation, and lifecycle gating
  • PATCH /v1/campaigns/{id}/status — pause, resume, or stop live campaigns (running | paused | stopped)
  • PATCH /v1/campaigns/{id}/flow/nodes/{nodeId} — live content-only node patch
  • GET /v1/flow-templates — starter flow graphs
  • GET /v1/campaigns/{id}?include=launch_state,lead_field_state — checklist observability without extra validate calls
  • POST /v1/campaigns/{id}/launch — start a draft campaign after backfilling enrollments
  • field_sync on flow saves — auto-declares merge-variable fields from email copy

Changed

  • GET /v1/campaigns list responses omit flow_data; use GET /v1/campaigns/{id} for the full flow
  • Campaign detail responses include computed flow_revision
  • POST /v1/campaigns/{id}/launch returns { 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_error with code flow_locked.
  • POST /v1/campaigns/{id}/lead-fields now 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:validate response 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.categorized when 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, and campaign.stopped webhooks (previously Client API only)
  • PATCH /v1/threads/{id} category updates emit reply.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 /docs into 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_category for uncategorized), tag_ids, date_from, date_to, has_reply_only (default true)
  • PATCH /v1/threads/{id} — partial update: category, conversation_status, read

Outbound messaging

  • POST /v1/threads/{id}/forward — queue forward job (forward_message_id required)
  • GET /v1/message-jobs/{id} — poll reply/forward job status
  • POST /v1/message-jobs/{id}/cancel — cancel queued/failed outbound job
  • POST /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 OOO
  • POST /v1/threads/{id}/replace-lead — replace thread lead; optional forward_message_id
  • GET /v1/thread-tags — list account thread tags
  • POST /v1/threads/{id}/tags:add / tags:remove — assign or remove tag on thread

Changed

  • POST /v1/threads/{id}/reply — optional in_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 endpoints
  • POST /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