Pricing

Connect ActiveCampaign to Google Sheets

ActiveCampaignSaved SearchDateDocumentAmount07/01INV-40214,280.0007/02INV-40221,860.5007/05INV-40239,140.0007/08INV-40242,375.2507/09INV-40256,902.7507/12INV-40263,118.40BrookedABCDateDocumentAmount123456707/01INV-40214,280.0007/02INV-40221,860.5007/05INV-40239,140.0007/08INV-40242,375.2507/09INV-40256,902.7507/12INV-40263,118.40Google SheetsActiveCampaign report
JW
James Whitfield

Export ActiveCampaign contacts to Google Sheets, including the custom fields that live on a separate resource and the account-specific API URL people miss.

ActiveCampaign's API is REST and straightforward, with one modelling decision that catches everyone: custom field values and tags are not properties on a contact, they are separate related resources. A plain contacts request therefore returns names and emails and none of the segmentation data you actually wanted. This guide covers joining those back together, plus the account-specific API URL and pagination.

The two-minute option: CSV export

In ActiveCampaign, open Contacts, apply a filter or pick a list, and use the export option to receive a CSV. Large exports are emailed rather than downloaded directly. Import into Sheets via File → Import. This includes custom fields as columns already, which is the work the API leaves to you.

ActiveCampaign to Google Sheets for free (Apps Script + API v3)

Get your API URL and key from Settings → Developer. The URL is account-specific, in the form https://youraccount.api-us1.com, and the key goes in an Api-Token header.

The pattern that makes the result useful: request include=fieldValues so custom field values come back alongside contacts, and fetch the fields endpoint once to map their numeric IDs to readable names.

Code.gs
function importActiveCampaignContacts() {  // Account-specific host, not a shared endpoint.  const API_URL = "https://youraccount.api-us1.com";  const API_KEY = "YOUR_API_KEY";  const headers = { "Api-Token": API_KEY };   // Custom field values reference their definition by numeric ID.  const fieldNames = {};  JSON.parse(UrlFetchApp.fetch(    API_URL + "/api/3/fields?limit=100", { headers }  ).getContentText()).fields.forEach((f) => {    fieldNames[f.id] = f.title;  });  const fieldIds = Object.keys(fieldNames);   const rows = [];  let offset = 0;  let total = null;   do {    // include=fieldValues inlines custom values; without it they are absent.    const url = API_URL + "/api/3/contacts"      + "?limit=100&offset=" + offset + "&include=fieldValues";     const body = JSON.parse(      UrlFetchApp.fetch(url, { headers, muteHttpExceptions: true })        .getContentText()    );    if (!body.contacts) throw new Error(JSON.stringify(body));     // fieldValues arrive as a sibling array keyed by contact, not nested    // inside each contact, so index them by contact ID first.    const valuesByContact = {};    (body.fieldValues || []).forEach((fv) => {      valuesByContact[fv.contact] = valuesByContact[fv.contact] || {};      valuesByContact[fv.contact][fv.field] = fv.value;    });     body.contacts.forEach((c) => {      const custom = valuesByContact[c.id] || {};      rows.push([        c.email,        c.firstName || "",        c.lastName || "",        c.phone || "",        c.cdate,        c.udate,        ...fieldIds.map((id) => custom[id] || ""),      ]);    });     // meta.total tells you how many exist; there is no next-page link.    total = Number(body.meta.total);    offset += 100;    Utilities.sleep(250);          // ~5 requests/second limit  } while (offset < total);   const header = ["Email","First name","Last name","Phone","Created","Updated",                  ...fieldIds.map((id) => fieldNames[id])];  const ss = SpreadsheetApp.getActiveSpreadsheet();  const sheet = ss.getSheetByName("Contacts") || ss.insertSheet("Contacts");  sheet.clearContents();  sheet.getRange(1, 1, 1, header.length).setValues([header]);  if (rows.length) {    sheet.getRange(2, 1, rows.length, header.length).setValues(rows);  }}

The detail worth noticing: included resources come back as sibling arrays at the top level of the response rather than nested inside each contact, so you have to index them by contact ID and join manually. Expecting them nested is why people conclude the include parameter did nothing.

Limits: around five requests a second per account, and Apps Script's six-minute cap, so large databases need chunking across scheduled runs. Tags require their own include or endpoint, joined the same way as field values.

The Brooked method

Brooked joins custom field values and tags onto contacts and resolves field IDs to their titles, so a contact export arrives as one wide, readable table. Contacts, lists, campaigns and deals can all import into one spreadsheet on a single schedule.

Step 1: Install Brooked

Install Brooked from the Google Workspace Marketplace. Free tier includes 100 imports per month, no credit card required.

Step 2: Connect ActiveCampaign

Choose ActiveCampaign in Brooked and enter your account-specific API URL and key from Settings → Developer. Pick contacts, lists, campaigns or deals, optionally filter by list or tag, and import.

Step 3: Schedule the refresh

Set the import to re-run daily so list growth, tag membership and campaign performance stay current in the sheet.

BrookedBrooked AI, ActiveCampaign
Live
Ask anything about your ActiveCampaign data…

Method comparison

MethodCostSetupRefreshTwo-wayBest for
ActiveCampaign CSV exportFree with plan2 minManualNoA one-off list export
Apps Script + API v3Free25 to 40 minTime-driven triggerDIYCustom fields and tag joins
Zapier / MakeFree tier limited · paid for volume10 to 15 minOn contact changeDIY (second workflow)Logging individual contact events
BrookedFree tier · Pro $29/user/mo~3 minScheduled (hourly, daily, weekly)YesScheduled list and campaign reporting

Common use cases

  • List growth: new contacts per source and per week in one table
  • Engagement audits: find contacts with no opens or clicks for re-engagement or suppression
  • Campaign reporting: open and click rates joined against send dates and segments
  • Data hygiene: spot missing or malformed custom field values across the database
  • Revenue attribution: join contacts against Stripe or CRM data already in the sheet

Troubleshooting common issues

Frequently asked questions

Where do I find my ActiveCampaign API URL and key?

Both are under Settings → Developer. The important detail is that the API URL is specific to your account, in the form https://youraccount.api-us1.com, and is not a shared endpoint. Requests go to that host with the key sent in an Api-Token header. Using a generic ActiveCampaign domain will simply fail to resolve to your data.

Why are my custom fields missing from contacts?

ActiveCampaign models custom field values as their own resource rather than properties on the contact. A plain contacts request returns only core fields. Either add include=fieldValues to inline them, or call the fieldValues endpoint separately, and fetch the fields endpoint too, because field values reference their definition by numeric ID rather than name.

Can I export ActiveCampaign contacts for free?

The CSV export in the interface is included with your plan and handles a one-off. For data that refreshes, the API is available on all plans and the Apps Script method here runs on a free trigger. Brooked's free tier covers 100 imports a month.

How does pagination work?

With limit and offset, where limit caps at 100. The response includes a meta.total count, so you can compute how many pages to expect up front. Increment offset by 100 until you have collected that total; the API does not return a next-page link.

Why are contact tags not on the contact?

Like custom fields, tags are a separate relationship. A contact does not carry a tag list; you either request include=contactTags,contactTags.tag or query the contactTags endpoint and join. This is a frequent cause of segmentation reports that come back empty.

Can I update contacts in ActiveCampaign from Sheets?

Yes for editable fields, and Brooked supports write-back, so bulk updates to custom fields or tags from a sheet are possible. Be careful with anything that triggers automations: adding a tag from a spreadsheet can enrol thousands of contacts into an automation and send real email.

Connect ActiveCampaign to Google Sheets today.

Free tier: 100 imports per month, no credit card required. Or see the full ActiveCampaign integration details.

Install Brooked free →

Get your spreadsheet hours back

Brooked sets up in seconds. Your team is querying live data before lunch.

Get started free