Skip to content
Preview: outbound-sdk is not on npm yet and its repository is private. Instantly and HeyReach are beta, Smartlead is unverified.What is verified
outbound-sdk
Esc
↑↓navigate↵open⌘Jpreview
On this page

Instantly adapter

Construct the Instantly adapter, know which key it needs, what it can and cannot read, what its reads cost in requests, the rate limits it keeps to and the assumptions it has not confirmed.

outbound-sdk/instantly reads Instantly’s API V2. It is an email adapter. It is beta: see what is verified.

Construct it

import { createOutbound } from "outbound-sdk";
import { instantly } from "outbound-sdk/instantly";

// Reads INSTANTLY_API_KEY when `apiKey` is left out. `workspaceId` is your own
// label for the client; it is part of every ref.
const outbound = createOutbound({
  adapter: instantly({ workspaceId: "acme" }),
});

// Nothing below makes a request.
console.log(outbound.provider, outbound.maturity, outbound.workspaceId);

console.log("webhook check:", outbound.capabilities["events.verify"]);

console.log("native reads:", Object.keys(outbound.native.catalog).length);

// The rate budget the SDK keeps to: the declared limit times `share` (0.5).
for (const snapshot of await outbound.limits()) {
  for (const window of snapshot.windows) {
    console.log(
      `${window.lane}: ${window.requests} per ${window.perMs / 1000}s`
    );
  }
}

// What a read costs in vendor requests, before making it.
for (const action of [
  "campaigns.get",
  "campaigns.list",
  "senders.health",
  "leads.list",
]) {
  console.log(action, "-", outbound.estimate({ action, records: 250 }).basis);
}
instantly beta acme
webhook check: secret
native reads: 105
default: 50 per 1s
default: 3000 per 60s
emails: 10 per 60s
labelPrediction: 250 per 2592000s
localBusinessFilters: 50 per 86400s
localBusinessSearch: 100 per 86400s
campaigns.get - 3 request(s) per page
campaigns.list - 3 page(s) of 100 at 3 request(s) each, plus 0 per record
senders.health - 3 request(s) per page
leads.list - 3 page(s) of 100 at 1 request(s) each, plus 0 per record
Option Default What it is
apiKey INSTANTLY_API_KEY An API V2 key.
workspaceId INSTANTLY_WORKSPACE_ID, then default Your own label for the client or workspace. It is part of every ref and may not contain a colon.
asWorkspace none For the key of an agency’s admin workspace: the id of the sub workspace to act as. Sent as the x-as-workspace header on every request.
baseUrl https://api.instantly.ai/api/v2 For a proxy or a test server.
capabilities what the adapter declares Overrides single capabilities.

The module also exports envVars (the two variable names), PROVIDER ("instantly") and NATIVE (the native catalog).

Key and scope

  • Create the key in Instantly under Settings, Integrations, API Keys. It is shown once. A V1 key does not work on V2.
  • A key with the all:read scope is enough for every read in the SDK, including the campaign tag reads.
  • One key belongs to one workspace. The exception is the key of a workspace group’s admin workspace, which can act as a sub workspace with asWorkspace. Without it, calls read the key’s own workspace.
  • Instantly answers 402 when the workspace’s plan does not include an endpoint. The adapter maps that to Unsupported, not to a failure worth retrying.

What it can and cannot do

Instantly declares every capability in the table except deletionDetection. Three values are weaker than the best case:

Capability Value Why
leads.since client POST /leads/list has no date filter, so the SDK filters after fetching and marks the result derived.
stats.bySender derived Per-mailbox numbers exist by day only. A total is summed by the adapter.
events.verify secret Instantly does not sign webhooks.

leads.lookup works by email only. A lead in Instantly has no LinkedIn profile, so a lookup by profile returns nothing and sends no request.

Rate limits

The adapter declares what Instantly publishes: 100 requests per second and 6,000 per minute per workspace, shared across keys and API versions. GET /emails has its own lane at 20 per minute, which bounds how fast messages and replies can be listed: at 100 emails a page, 2,000 emails a minute at the declared limit and half that at the default share. Three native reads carry daily or monthly limits of their own (leadLabelPrediction, localBusinessFilters, localBusinessSearch), counted in the process only.

The output above shows the budget after the default share of 0.5. See rate limits.

What reads cost

Read Vendor requests
One campaign, or one page of campaigns 3: the campaign or page, its tag assignments, then the tags’ names
A campaign’s settings, sequence 1 each
A campaign’s senders 2 per page of mailboxes: every mailbox is read and filtered
Sender health 3: the account, a live DNS test, its warmup analytics
Find a person by email 1 per page
Any other listing 1 per page of 100
A message body that the list cut short 1 more for that email, on the 20 per minute lane

To make a campaign read cost 1 request, turn the tag reads off with capabilities: { tags: false }. Campaign tags are then empty. outbound.estimate reads the declared table and still reports 3.

A page of campaigns reads at most 10 pages of 100 tag assignments. A page with more than that is refused with a Provider error, not returned with tags missing.

Vendor behavior that affects your code

  • Steps count from 0. An email names its step as sequence_step_variant, each counted from 0. This was seen live and is not documented.
  • A reply has no lead ref. A received email carries the lead’s address but no lead id, so reply.leadRef is null. Match on the address.
  • The email list is not every email. A lead on its sixth step was seen with one sent email and two replies. A timeline or thread can be shorter than the sequence; lead.engagement.stepIndex says how far the lead got.
  • A listing ends one request late. Instantly sends a next-page marker on the last page too, and the page after it is empty.
  • Open counts come back even with open tracking off. The analytics response does not say whether tracking was on.
  • isAuto is always null on replies, and daily stats rows have a null timezone.
  • A lead list’s size is null. The count costs one more request per list; the native read leadListVerificationStats returns it for one list.
  • Webhooks need Instantly’s Hyper Growth plan or above, by Instantly’s documentation.

Caveats

outbound.caveats is the adapter’s own list of what it assumes and has not confirmed. Read it before relying on a field.

import { createOutbound } from "outbound-sdk";
import { instantly } from "outbound-sdk/instantly";

const outbound = createOutbound({ adapter: instantly() });

// What the adapter assumes and has not confirmed. No request is made.
for (const caveat of outbound.caveats) {
  console.log(`- ${caveat}\n`);
}
- The reads were run once, read-only, against one live workspace on 2026-11-14. Fixtures are still built from the API V2 documentation, and subsequences, a campaign's own webhooks and a second page of most lists were not exercised because that workspace had none or too few.

- Instantly sends next_starting_after on a last page too (seen live), and the page after it is empty. A listing therefore ends one request late; the small lookups for tags and labels stop at the first page that is not full.

- A reply's category is read from the email's i_status using the lead interest status values (seen live: 0, 1, -1 and -2). A received email carries the lead's address but no lead id, so a reply's leadRef is null; a sent message has one. Instantly does not mark an email as automatic, so isAuto is null.

- An email's step and a lead's last step are ids of the form sequence_step_variant, each counted from 0 (seen live, not documented); any other form gives a null step. Step analytics count steps from 0 too, and can hold a row with no step for counts Instantly could not tie to one.

- Whether reply_count includes automatic replies is not documented; replied uses the unique reply count and repliedAuto the unique automatic count.

- Open counts are returned even when a campaign has open tracking turned off, because the analytics response does not say whether tracking was on.

- Daily analytics rows do not state their timezone, so timezone is null.

- Campaign tags need two more reads (custom-tag-mappings, then custom-tags) and so a key that can read both; an all:read key can. Turn the tags capability off to skip them: campaign tags are then empty.

- A page of campaigns reads at most 10 pages of 100 tag assignments. A page with more assignments than that is refused with a Provider error, not returned with tags missing.

- A step's delay is documented as the wait before the next email, so a step's delayDays is the delay of the step before it; the first step of a campaign waits 0 and the first step of a subsequence waits its pre_delay.

- A campaign's settings, sequence and senders each read the whole campaign object. A sending window's days are assumed to be numbered 0 for Sunday to 6 for Saturday; the documentation only shows the keys 0 to 6.

- A campaign's senders are found by reading every mailbox and keeping those named in the campaign's email_list or carrying a tag in its email_tag_list, at two requests per page of mailboxes. A campaign that names tags only is filtered by Instantly.

- Sender health is three reads: the account, POST /accounts/test/vitals (a live DNS test, which Instantly may throttle) and POST /accounts/warmup-analytics. The warmup window is not documented. blacklisted and checkedAt are not reported. A DNS record Instantly could not resolve is null, not false.

- connected is true for an Active account and false for a Connection Error; every other account status is null.

- A lead's engagement.stepIndex is the last step sent, read from its status summary; lastOpenedAt is null when open tracking is off. A lead has no tags in Instantly, and the lead object was seen without a job title.

- A lead is found by email with the contacts filter of POST /leads/list, documented only as the emails a lead needs to have. Whether it compares whole addresses and ignores case is not documented and was not run live, so the adapter keeps only the records whose address equals the one asked for, and a page can hold fewer records than Instantly returned. One record comes back per campaign or list the address is in. A lead has no LinkedIn profile: a lookup by profile returns nothing and sends no request.

- A lead's interest is the documented label for lt_interest_status. A custom label costs one more read (lead-labels, first 100 labels), made only when a page holds a status that is not a built-in one; the same read gives a reply with a custom status its category.

- A lead list's size is null: the list object has no count, and the count costs one more read per list. The native read leadListVerificationStats returns total_leads for one list. kind is always leads.

- Native reads return what Instantly sends, unmapped. dfyAccounts never asks for passwords. Instantly refuses per-mailbox daily analytics (413) on a large workspace unless a mailbox is named, which the per-sender stats always do.

- The daily limits of leadLabelPrediction, localBusinessFilters and localBusinessSearch are counted in this process only; a restart forgets what was used.

- A webhook made for a custom lead label has no event_type, so its events are empty; custom_interest_value stays in raw. The headers Instantly sends with a delivery can carry a credential, so they are left out of a webhook record's raw. The native read webhook returns them and is not offered to the vendor_read tool; outbound.raw returns a response as it is.

- A truncated message body costs one more read of that email (GET /emails/{id}) when bodies are asked for. That read is assumed to share the 20-per-minute limit of the email list.

- Messages of a thread are found with the documented search value thread:<id>.

- Instantly's email list does not hold every email a campaign sent (seen live: a lead on its sixth step had one sent email and two replies). A timeline or a thread can therefore be shorter than the sequence; a lead's engagement.stepIndex says how far it got. A received email carries the step it replies to.

Webhooks

Verification mode: secret. Instantly does not sign payloads, so put a secret in the webhook URL or in an x-webhook-secret header and pass the same value as secret when parsing. See webhook verification.

Coverage

Instantly coverage lists all 268 endpoints of the API V2 reference: 22 behind normalized reads, 105 in the native catalog, 4 excluded and 137 writes. The research note the adapter was written from is in the repository at docs/research/instantly.md.

Was this page helpful?