Instantly Upgrade Migration: API v1 to v2
Overview
Migrate from Instantly API v1 (deprecated January 2026) to API v2. Key changes: Bearer token auth replaces query-string API keys, REST-standard endpoints replace legacy paths, scoped API keys replace single global key, and cursor-based pagination replaces offset pagination. Existing v1 integrations via Zapier/Make continue working, but new integrations must use v2.
Prerequisites
- Existing Instantly API v1 integration
- Access to Instantly dashboard to generate v2 API keys
- Understanding of Bearer token authentication
Migration Map
Authentication Change
const v1Url = `https://api.instantly.ai/api/v1/campaign/list?api_key=${API_KEY}`;
const v2Response = await fetch("https://api.instantly.ai/api/v2/campaigns", {
headers: { Authorization: `Bearer ${API_KEY}` },
});
Endpoint Migration Table
| Operation | v1 Endpoint | v2 Endpoint | Method Change |
|---|
| List campaigns | GET /api/v1/campaign/list | GET /api/v2/campaigns | Same |
| Get campaign | GET /api/v1/campaign/get | GET /api/v2/campaigns/{id} | Query -> Path param |
| Create campaign | POST /api/v1/campaign/create | POST /api/v2/campaigns | REST standard |
| Launch campaign | POST /api/v1/campaign/launch | POST /api/v2/campaigns/{id}/activate | New path |
| Pause campaign | POST /api/v1/campaign/pause | POST /api/v2/campaigns/{id}/pause | New path |
| Add leads | POST /api/v1/lead/add | POST /api/v2/leads | Simplified |
| List leads | GET /api/v1/lead/list | POST /api/v2/leads/list | GET -> POST |
| Delete leads | POST /api/v1/lead/delete | DELETE /api/v2/leads/{id} | REST standard |
| Get analytics | GET /api/v1/analytics/campaign | GET /api/v2/campaigns/analytics | New path |
| List accounts | GET /api/v1/account/list | GET /api/v2/accounts | Simplified |
Request Body Changes
const v1Body = {
api_key: "your-key",
name: "Campaign Name",
};
const v2Body = {
name: "Campaign Name",
campaign_schedule: {
start_date: "2026-04-01",
schedules: [{
name: "Business Hours",
timing: { from: "09:00", to: "17:00" },
days: { "1": true, "2": true, "3": true, "4": true, "5": true, "0": false, "6": false },
timezone: "America/New_York",
}],
},
sequences: [{
steps: [{
type: "email",
delay: 0,
variants: [{ subject: "Hello {{firstName}}", body: "Hi {{firstName}}..." }],
}],
}],
};
Lead Operation Changes
const v1AddLeads = {
api_key: "your-key",
campaign_id: "campaign-uuid",
leads: [
{ email: "user@example.com", first_name: "Jane" },
],
};
const v2AddLead = {
campaign: "campaign-uuid",
email: "user@example.com",
first_name: "Jane",
skip_if_in_workspace: true,
verify_leads_on_import: true,
custom_variables: { role: "CTO" },
};
Instructions
Step 1: Audit Existing v1 Calls
set -euo pipefail
grep -rn "api/v1/" src/ --include="*.ts" --include="*.js" --include="*.py" || echo "No v1 calls found"
grep -rn "api_key=" src/ --include="*.ts" --include="*.js" --include="*.py" || echo "No query-string keys found"
Step 2: Create Migration Adapter
export class InstantlyV1ToV2Adapter {
private apiKey: string;
private baseUrl = "https://api.instantly.ai/api/v2";
constructor(apiKey: string) {
this.apiKey = apiKey;
}
private async request<T>(path: string, options: RequestInit = {}): Promise<T> {
const res = await fetch(`${this.baseUrl}${path}`, {
...options,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.apiKey}`,
...options.headers,
},
});
if (!res.ok) throw new Error(`Instantly ${res.status}: ${await res.text()}`);
return res.json() as Promise<T>;
}
() {
.();
}
() {
.();
}
() {
.(, { : });
}
() {
.(, { : });
}
() {
results = [];
( lead leads) {
result = .(, {
: ,
: .({
: campaignId,
: lead.,
: lead.,
: ,
}),
});
results.(result);
}
results;
}
() {
.();
}
}
Step 3: Pagination Migration
async function* paginateV2<T extends { id: string }>(
path: string,
pageSize = 100
): AsyncGenerator<T[]> {
let startingAfter: string | undefined;
while (true) {
const qs = new URLSearchParams({ limit: String(pageSize) });
if (startingAfter) qs.set("starting_after", startingAfter);
const page = await instantly<T[]>(`${path}?${qs}`);
if (page.length === 0) break;
yield page;
startingAfter = page[page.length - 1].id;
if (page.length < pageSize) break;
}
}
Step 4: New v2 Features to Adopt
await instantly("/api-keys", {
method: "POST",
body: JSON.stringify({ name: "analytics-only", scopes: ["campaigns:read"] }),
});
await instantly("/subsequences", {
method: "POST",
body: JSON.stringify({
parent_campaign: campaignId,
name: "Re-engage interested leads",
conditions: { crm_status: [1] },
}),
});
await instantly("/inbox-placement-tests", {
method: "POST",
body: JSON.stringify({
name: "Pre-launch deliverability test",
email_subject: "Test Subject",
email_body: "Test body content",
: ,
}),
});
(, {
: ,
: .({
: [, ],
}),
});
Migration Checklist
Error Handling
| Error | Cause | Solution |
|---|
401 on v2 | Using v1 key format | Generate new v2 Bearer token |
404 on v2 path | Using v1 endpoint path | Check migration table above |
422 on lead add | New validation rules in v2 | Add required fields per v2 schema |
| Missing pagination data | Using skip instead of starting_after | Convert to cursor pagination |
Resources
Next Steps
For CI/CD integration, see instantly-ci-integration.