Adapters
What an adapter is, the four that ship, what each can and cannot do in one table checked against the code, and how much of each vendor's API is covered.
An adapter is the part that knows one vendor. It describes each request and maps the response to the SDK’s records. It never retries, sleeps or walks pages: createOutbound does that around it, the same way for every vendor. You pass one adapter to createOutbound and call the same methods whichever vendor is behind it (see usage).
Each vendor adapter has two layers: normalized reads that mean the same thing on every vendor, and a typed native catalog of the vendor’s own reads for everything else.
The adapters
| Adapter | Import | Channel | Key variable | Page |
|---|---|---|---|---|
| Instantly (API V2) | outbound-sdk/instantly |
INSTANTLY_API_KEY |
Instantly | |
| Smartlead | outbound-sdk/smartlead |
SMARTLEAD_API_KEY |
Smartlead | |
| HeyReach | outbound-sdk/heyreach |
HEYREACH_API_KEY |
HeyReach | |
| In-memory | outbound-sdk/memory |
both | none | In-memory |
combine from outbound-sdk/combine puts several of them, or several keys for one of them, behind one instance.
Every vendor factory takes the same core options:
| Option | Default | What it is |
|---|---|---|
apiKey |
the key variable above | The vendor key. A missing key throws when the adapter is constructed. |
workspaceId |
<PROVIDER>_WORKSPACE_ID, then default |
Your own label for the client or workspace. It is part of every ref (provider:workspace:id). |
baseUrl |
the vendor’s API | For a proxy or a test server. |
capabilities |
what the adapter declares | Overrides single capabilities, for example to turn one off on a plan without the endpoint. |
Constructing an adapter makes no request.
How far each is verified
One line each, from what is verified, which is the page to read before relying on a number:
- Instantly is
beta: its reads were run, read-only, against one live workspace on 2026-11-14. - HeyReach is
beta: its reads were run, read-only, against one live workspace on 2026-11-14. - Smartlead is
unverified: written from Smartlead’s public documentation and tested offline only. - In-memory is
stable: it has no vendor behind it.
outbound.maturity reports this at run time, and outbound.caveats lists what the adapter assumes and has not confirmed. Each adapter’s page shows its caveats in full.
What each adapter can do
A filter is server (the vendor filters), client (the SDK filters after fetching and marks the result derived) or no. A stat is exact, derived or no. A call that needs a no is refused as Unsupported before any request leaves. The repository’s test suite checks this table against the adapters’ own declarations (packages/outbound-sdk/test/adapters-doc.test.ts). See capabilities for what each key gates and how to branch on it.
| Capability | Instantly | Smartlead | HeyReach |
|---|---|---|---|
blocklist |
yes | yes | yes |
campaigns.settings |
yes | yes | no |
campaigns.status |
server | client | server |
deletionDetection |
no | no | no |
events.parse |
yes | yes | yes |
events.verify |
secret | signature | secret |
leads.byCampaign |
yes | yes | yes |
leads.engagement |
yes | no | no |
leads.get |
yes | yes | yes |
leads.list |
yes | yes | no |
leads.lookup |
yes | yes | yes |
leads.since |
client | client | server |
leads.timeline |
yes | yes | yes |
lists.leads |
yes | no | yes |
lists.list |
yes | yes | yes |
messages.body |
full | full | full |
messages.byThread |
yes | yes | yes |
messages.list |
yes | yes | yes |
messages.since |
server | client | server |
native |
yes | yes | yes |
replies.category |
vendor | vendor | no |
replies.list |
yes | yes | yes |
replies.since |
server | client | server |
senders.byCampaign |
yes | yes | yes |
senders.get |
yes | yes | yes |
senders.health |
yes | yes | yes |
senders.list |
yes | yes | yes |
senders.warmup |
yes | yes | no |
sequence |
yes | yes | yes |
sequence.subsequences |
yes | yes | no |
stats.byDay |
exact | exact | exact |
stats.bySender |
derived | exact | exact |
stats.byStep |
exact | exact | no |
stats.campaign |
exact | exact | exact |
stats.workspace |
exact | exact | exact |
tags |
yes | yes | no |
webhooks.list |
yes | yes | yes |
workspaces |
yes | yes | no |
The in-memory adapter declares everything except native, events.parse, events.verify and deletionDetection.
What stays no, and why
What a vendor’s API cannot answer stays no. The SDK does not fill the gap with a guess.
- HeyReach has no campaign settings, subsequences, tags or per-lead counts. Its API can set a campaign’s schedule but not read it, a campaign has one sequence tree, no endpoint lists the workspace’s tags, and no lead carries open or reply counts.
- HeyReach has no per-step stats and no warmup: LinkedIn accounts are not warmed up.
- HeyReach replies carry no category. Its auto-tags describe the lead, not a message; the newest one is the lead’s
interest. - HeyReach has no list of every lead. Leads are read per campaign or per saved list, or found by profile or email.
- HeyReach cannot list workspaces with a workspace key.
- Smartlead cannot list the leads of a saved list, and its per-lead counts are documented only as a placeholder response.
- Instantly’s per-mailbox stats are
derived: its mailbox analytics exist per day only, so the adapter sums them. - No adapter reveals deleted records. The reconcile plugin compares what you stored with what the vendor still lists.
How much of each vendor’s API is covered
Every endpoint in each vendor’s API reference is listed on its coverage page with how the SDK reaches it: through a normalized read, through the native catalog, excluded with a reason, or a write the SDK does not make.
| Vendor | Endpoints | Normalized | Native | Excluded | Writes |
|---|---|---|---|---|---|
| Instantly | 268 | 22 | 105 | 4 | 137 |
| Smartlead | 230 | 27 | 78 | 20 | 105 |
| HeyReach | 106 | 17 | 25 | 3 | 61 |
A read is excluded only when it returns secrets (API keys, mailbox passwords, proxy credentials), duplicates another endpoint’s data exactly, reports nothing about campaigns, leads, senders or results (billing, marketplace pricing, search filter lists), or returns a file the SDK cannot carry. Reads that spend vendor credits are listed as writes.
Rate limits each adapter declares
| Adapter | Declared limit | Stricter lanes |
|---|---|---|
| Instantly | 100 per second and 6,000 per minute, per workspace | GET /emails: 20 per minute |
| Smartlead | 60 per minute (the limit of a client key; an admin key is documented at 200) | three endpoints at 10 per minute |
| HeyReach | 15 per 2 seconds (seen live, not documented) and 300 per minute | the organization API: 300 per minute |
The SDK spends share (default 0.5) of these numbers, because the vendor’s limit is shared with the customer’s other tools. Rate limits explains the budget, and what is verified says which of these numbers were measured.
The provider catalog and choosing an adapter at run time
outbound-sdk/providers lists every provider with its channels, its maturity and the environment variables it reads, without importing any adapter. outbound-sdk/loader builds the adapter that OUTBOUND_SDK_PROVIDER names, reading that adapter’s own key from the environment; the CLI picks its adapter this way.
import { createOutbound } from "outbound-sdk";
import { fromEnv, loadableProviders } from "outbound-sdk/loader";
import { providers } from "outbound-sdk/providers";
// The catalog imports no adapter, so it is safe in a config screen.
for (const provider of providers) {
const required = provider.envVars
.filter((variable) => variable.required)
.map((variable) => variable.name);
console.log(provider.id, provider.maturity, provider.channels, required);
}
// fromEnv() builds the adapter OUTBOUND_SDK_PROVIDER names, or the one you
// pass. Only that adapter's code is loaded.
console.log(loadableProviders());
const outbound = createOutbound({
adapter: await fromEnv({ provider: "memory" }),
});
const campaigns = await outbound.campaigns.list();
console.log(outbound.provider, campaigns.items.length);
instantly beta [ 'email' ] [ 'INSTANTLY_API_KEY' ]
smartlead unverified [ 'email' ] [ 'SMARTLEAD_API_KEY' ]
heyreach beta [ 'linkedin' ] [ 'HEYREACH_API_KEY' ]
memory stable [ 'email', 'linkedin' ] []
[ 'heyreach', 'instantly', 'memory', 'smartlead' ]
memory 2
Writing an adapter
The adapter contract is the Adapter type exported from outbound-sdk. The checklist for adding one is in the repository, in docs/new-adapter.md, with the research note each existing adapter was written from under docs/research/.