SendBeam

CSV Import

Import contacts in bulk from a CSV file, with tags, custom fields, consent records and opt-out statuses.

View as Markdown

CSV import lets you add hundreds or thousands of contacts to SendBeam in a single operation. Whether you are moving an audience from another email platform, uploading a list from your CRM, or bringing in attendees from an event, the importer handles the heavy lifting — including tags, custom fields, the original opt-in dates, proof of consent and the people who have already unsubscribed.

The import is simple: prepare a CSV with the right column headers, upload it, and review the result. Each stage is described below.

Preparing your CSV file

  • The file must be in CSV format (.csv extension), UTF-8 encoded. Comma, semicolon and tab delimiters are detected from the header row.
  • The first row must be a header row with column names.
  • The file needs a column holding the email address. Header names are matched case-insensitively, with spaces and punctuation treated as underscores, so Email, First Name and Opt-In Time all work. Using the names below means the mapping screen is right first time; anything else you correct there in a click.
  • Quoted cells may contain commas, quotes (doubled) and line breaks.
  • The file may be at most 5 MB — roughly 50,000 rows. Larger uploads are refused; split the CSV and import it in parts.
lightbulb
Download the example file from the import page to see exactly the format SendBeam expects. It's the fastest way to get started without formatting errors.

Recognised columns

ColumnWhat it does
email Required. Up to 254 characters. email_address is accepted as an alias.
first_name, last_name Optional, up to 100 characters each; used by the {"{{first_name}}"} and {"{{last_name}}"} merge tags.
status subscribed, unsubscribed, bounced, complained or pending. Aliases: active → subscribed, cleaned → bounced, cancelled / canceled → unsubscribed, junk / spam → complained, unconfirmed → pending. A blank cell (or no column) takes the default status chosen on the import page. Rows marked pending are never imported — the person has not confirmed — and are counted as pending rows. Any other value makes the row invalid.
tags Tag names separated by ; or | (for example vip;beta). Up to 20 per row, names up to 60 characters. Tags are matched to existing tags case-insensitively and created when missing (at most 200 distinct new names per import).
subscribed_at The original opt-in time, kept as the contact's subscription date. Aliases: opted_in_at, signup_at, optin_time, created_at. Accepted formats: ISO 8601 (2024-01-05T10:00:00Z), YYYY-MM-DD HH:MM:SS, YYYY-MM-DD, DD/MM/YYYY, and Unix timestamps. Values without a time zone are treated as UTC. A date in the future is clamped to now; an unreadable value is treated as blank, which means "now".
unsubscribed_at When the person opted out (aliases opted_out_at, unsub_time). Used for unsubscribed, bounced and complained rows; when absent, the row's subscribed_at or the import time is used.
source A per-row source label (up to 40 characters) that overrides the source set on the import page. Rows with a blank cell take the page's value (default import).
language The language the person reads in, as a two-letter code (fr, de; fr-FR and pt_BR are reduced to the code). Aliases lang, locale, language_code, preferred_language. A value that is not a language is left blank rather than failing the row. On an existing contact it fills an empty language; it replaces one only when the import is set to update existing contacts.
consent_ip, consent_at, consent_source Proof of consent from your previous platform: the IP address the person signed up from, when they confirmed (aliases confirm_time, opt_in_confirmed_at; any of the date formats above), and the form or notice they saw. These are stored as the custom fields consent_ip, consent_confirmed_at (normalised to ISO 8601) and consent_source, so they travel with the contact and appear in exports.
any other column Becomes a custom field keyed by the lower-cased, snake_cased header (Company Name → company_name), available as {"{{custom_fields.company_name}}"} and in segment rules. Values are stored as text, cut to 200 characters; an empty cell stores nothing. A contact may hold at most 50 custom fields, so columns beyond that, columns whose key would be empty or repeated, and headers over 64 characters are ignored and listed under columns ignored in the result.

Sample file

email,first_name,last_name,status,tags,subscribed_at,source,consent_ip,consent_at,consent_source,company
alice@example.com,Alice,Smith,subscribed,vip;beta,2024-01-05 10:00:00,footer-form,203.0.113.9,2024-01-05 10:02:11,Footer newsletter form,Acme Ltd
bob@example.com,Bob,Jones,subscribed,,2023-11-20,,,,,
carol@example.com,Carol,,unsubscribed,,2022-03-01,,,,,
dave@example.com,Dave,,cleaned,,2021-07-14,,,,,
erin@example.com,Erin,,pending,,2026-08-30,,,,,

This file creates Alice (tagged vip and beta, with her consent record and a company custom field), Bob, Carol as unsubscribed and Dave as bounced — both added to the suppression list with their original dates — and leaves Erin out because she never confirmed.

A row whose email is missing, malformed or too long, whose name exceeds the limit, or whose status is not one of the values above, is counted as invalid and not imported. If the same address appears more than once in the file the rows are merged: the strongest status wins (complained over bounced over unsubscribed over subscribed) and tags and fields are combined.

Statuses and the suppression list

Rows with status unsubscribed, bounced or complained are imported as contacts with that status and recorded on the workspace's suppression list. That is how you carry your opt-outs across: campaigns never reach them, and a later import can never bring them back as subscribed. They are a compliance record rather than audience, so they do not count towards your plan's contact limit — only rows that will be subscribed do.

Addresses already on the suppression list — people who previously unsubscribed, bounced, complained or were deleted in this workspace — are imported as unsubscribed whatever the file says. They can only return by subscribing again through a signup form.

Uploading the file

  1. Go to Contacts in the sidebar, then click Import near the top of the page.
  2. Drop your CSV file onto the upload area, or click it to browse for the file.
  3. Check the columns. Every column in the file is listed with a few sample values and what it will become: a contact field (matched from the header — Primary email, a single Name column and a Surname are understood too), one of your existing custom fields (matched by key or label), or Ignore. A column SendBeam does not recognise is ignored until you map it, so a MEMBER_RATING or an internal id never becomes a field by accident. Choose New custom field… to keep a column as a field of its own, with a key and a type — a Birthday column becomes a Date field, a seats column a Number — and it is declared under Settings → Custom fields as part of the import. Values that do not fit the type are left out of that row and reported; nothing is bent to fit. Exactly one column must be mapped to Email, and a field can be fed by one column only.
  4. Choose the default status for rows without a status — Subscribed or Unsubscribed. Use Unsubscribed for a suppression list or an audience that has not opted in.
  5. Optionally set a source label (default import) for rows that do not carry their own, so you can find this batch later in segments.
  6. Optionally enter tags for every contact (separated by ;) — handy for marking a migration batch — and pick a list to add the subscribed contacts to. On a double opt-in list (and every list in a Free-plan workspace) they are added as awaiting confirmation; the import never sends confirmation emails, so send them yourself from the list page if you need to re-confirm an audience.
  7. Leave Skip duplicate emails ticked to keep existing contacts' names, or untick it to refresh their names and source from the file (see Handling duplicates below).
  8. Leave Run automations for these contacts unticked unless you mean it. An import brings people in; it does not greet them. Tick it and every contact the file creates enters your Contact created automations, and every contact it adds to a list enters that list's Joined a list automations — which is almost never what you want when you are moving an existing list in, since those people were welcomed long ago somewhere else. The summary tells you how many enrolments it started.
  9. Click Import. SendBeam checks the account's remaining contact allowance before it reads the file, reads only as many subscribed rows as the plan has room for, then creates the contacts in batches. Through the API (no mapping screen) the headers are read as described above and every unrecognised column becomes a custom field; send a mapping to get the same control as the screen — see the API reference.
  10. The result appears on the same page when the import finishes. Keep the page open until it does.
info
Any active automation with the Contact Created trigger enrols every newly created contact whose status is Subscribed, so pause a welcome sequence first if you do not want it to fire for an existing audience — or import with the default status set to Unsubscribed. A List Joined automation fires for contacts added to a single opt-in list; on a double opt-in list it waits until they confirm.
warning
Plan limit. If the number of subscribed rows in the file is more than the contact slots left on your plan, the whole import is refused with a message telling you how many slots remain — even if some rows would have been skipped as duplicates. Rows with an opt-out status never count. Split the file or upgrade under Billing & Plans.

Handling duplicates

When an email address in your CSV already exists in the workspace, the Skip duplicate emails option decides what happens to the contact's names and source:

  • Ticked (recommended) — the existing contact's first name, last name and source are left unchanged and the row is counted as a skipped duplicate.
  • Unticked — the existing contact's first name, last name and source are refreshed from the file. Use this when your CSV carries fresher names.

Either way, the file's data is added to existing contacts: its tags are attached, its custom fields are merged in (keys the file carries take the file's value; other keys are kept), and a currently subscribed contact whose row says unsubscribed, bounced or complained is moved to that status and suppressed — an opt-out in the file is always honoured. The reverse never happens: an import cannot turn an unsubscribed contact back into a subscribed one, and a complaint is never downgraded to a plain unsubscribe. Contacts changed this way are counted as updated. Re-importing a file is therefore safe to repeat.

Migrating from another platform

Export your audience from the old platform including the unsubscribed and cleaned members and their signup dates, rename the headers to the names above where they differ (most exports call the address column Email Address and the opt-in date OPTIN_TIME or CONFIRM_TIME), and import the whole file in one go. Keeping the original subscribed_at and consent columns matters: they are your evidence that each person opted in, and the opt-out rows keep you from ever emailing someone who left. If you would rather we did the move with you, the Move-in guide walks through the whole process — export, domain setup, import and the first send.

Import from Mailchimp, MailerLite, Kit, Brevo or EmailOctopus

If your audience is on Mailchimp, MailerLite, Kit, Brevo or EmailOctopus you can skip the export altogether. Go to Contacts → Import → Import from another platform, paste an API key from the old platform and click Connect. SendBeam checks the key with a single call and shows the account, its audiences / groups / tags / lists with subscriber counts, and exactly what will and will not come across. Pick what to bring over, optionally a tag for the batch and a SendBeam list, and click Import.

Where to create the key

  • Mailchimp — profile icon → Account & billing → Extras → API keys → Create A Key. The key ends in a data-centre suffix such as -us21; SendBeam reads the data centre from it.
  • MailerLite (the new MailerLite, not Classic) — Integrations → MailerLite API → Generate new token.
  • Kit — Settings → Developer → API Keys; use the v4 key.
  • Brevo — in your account settings, under SMTP & API → API Keys → Generate a new API key.
  • EmailOctopus — under Account → API, create a new API key.

None of the five platforms issues a truly read-only key, so create one just for the import and delete it on the old platform once you are done. SendBeam only ever reads with it: the key is held in your browser tab for the visit, sent with each request, and never stored, logged or displayed again. Nothing on the old platform is changed.

What is imported

  • Contacts with email, first and last name and their status. Mailchimp cleaned → bounced; MailerLite junk → complained; Kit cancelled → unsubscribed. Archived and transactional Mailchimp members are skipped.
  • Unsubscribed, bounced and complained addresses are recorded on the suppression list before any contact is written and are imported as contacts with that status. They never count towards your contact limit.
  • Unconfirmed / pending people (Mailchimp pending, MailerLite unconfirmed, Kit inactive, EmailOctopus pending) are counted but never imported — they did not confirm.
  • Original opt-in date as subscribed_at, and — where the platform exposes it — the consent evidence: IP address, confirmation time and signup source, stored as the custom fields consent_ip, consent_confirmed_at and consent_source. Mailchimp and MailerLite provide these; Kit provides the signup source (form, landing page, API, referrer, with UTM values as custom fields) but no IP or confirmation time; Brevo's consent IP is only known for people who later unsubscribed; EmailOctopus provides neither IP nor confirmation time, only the signup source on a double opt-in list. A Mailchimp confirmation time is only carried for double opt-in audiences; on single opt-in audiences the opt time is kept as mailchimp_timestamp_opt. MailerLite's unsubscribed_at is carried as the opt-out time.
  • Tags: Mailchimp tags, MailerLite groups, Kit tags, Brevo lists and EmailOctopus tags become SendBeam tags (created when missing). Kit tags arrive with each subscriber, so any number of tags is mapped in one pass. Mailchimp audience group interests become interest:<name> tags, and members with more than 50 tags have their full set fetched separately (up to 40 such members per run; the rest carry the custom field mailchimp_tags_truncated and the result reports tags truncated).
  • Custom fields: merge fields, MailerLite fields, Kit fields, Brevo attributes and EmailOctopus fields become custom fields keyed by their snake_cased name (up to 30 per contact, values cut to 200 characters; address-type fields are dropped). Mailchimp GDPR marketing permissions become mc_permission_<name> fields.

What is not

  • Campaign history, templates, automations, forms, landing pages, segments and statistics stay on the old platform.
  • Mailchimp does not expose spam complaints separately (they are folded into cleaned); Kit exposes no consent IP or confirmation timestamp; paid Kit newsletters are not carried over; EmailOctopus does not expose bounces or complaints as contact statuses at all (they only appear in campaign reports), so those addresses arrive as subscribed or unsubscribed.
  • Nothing is deleted or modified on the old platform, and no email is sent to anyone by the import.

Caps and re-running

A run reads the source one page at a time and writes each page as it goes, so a large audience never sits in memory. Each run handles up to 25,000 contacts (rounded up to the end of the source page in progress), makes at most 250 requests to the old platform and runs for at most about 45 seconds. When a cap is hit — or your plan has no room for the next page of subscribed contacts — the run stops between pages, reports everything written so far with truncated: true, and offers Continue import, which resumes from exactly that page. Re-running is always safe: contacts already in the workspace are skipped as duplicates, tags and custom fields are added, an opt-out in the source always wins over a subscribed contact here, and a stronger suppression reason is never downgraded. The same audience can be imported again later to pick up new signups.

The two calls behind the page are available to your own code with an API key that has contacts:write (Pro or Business): POST /api/v1/imports/connect with {"source", "credentials": {"api_key"}}, then POST /api/v1/imports/run with the same plus list_ids and options (tags, list_id, max_contacts, cursor from a previous run's next_hint). See the API reference.

Addresses that cannot receive mail

Two kinds of address are skipped before a row becomes a contact. The first needs no lookup: placeholders that can never belong to a subscriber — anything on example.com, example.net or example.org (or a subdomain of them), anything on a reserved .test, .invalid, .localhost or .example domain, and the abuse@ and postmaster@ mailboxes that every domain runs for its mail administrators. Sample rows left in an export and addresses scraped from a contact page are the usual sources. Each one is skipped whatever check_domains says, and the summary names them (the first twenty) with the reason, so you can fix the file rather than wonder why a row is missing. A campaign's Review step and its page make the same check on the audience and list any that are already contacts.

Every import also checks the domain of each remaining address. A domain that does not exist, that publishes a "null MX" (an explicit no mail here), or that has neither mail servers nor an address of its own cannot be delivered to, so those rows are skipped and the summary lists the domains with the most rows lost — typos like gmial.com and companies that have closed are the usual culprits. Only a definite DNS answer skips a row; a slow or failing lookup lets the row through. Well-known providers are never looked up, and an import with more than 500 distinct domains checks the first 500.

This is a domain check, not a mailbox check: a real domain with a mistyped local part still imports and bounces later, at which point the contact is marked bounced and suppressed as usual. Pass check_domains=false on the API to skip the check.

Import results summary

Once your import finishes, SendBeam displays a summary so you can verify everything went as expected:

  • Contacts imported — new contacts created.
  • Existing contacts updated — contacts that already existed and received custom fields, a refreshed name or an opt-out status from the file.
  • Duplicates skipped — rows whose email address already existed in the workspace.
  • Added to the suppression list — unsubscribed, bounced and complained addresses recorded so they are never mailed.
  • Pending rows not imported — rows with status pending.
  • Invalid rows — rows dropped because the email was missing or malformed, a value exceeded its limit, or the status was unrecognised.
  • Tags attached (and new tags created) — contact–tag links made by this import.
  • Custom fields — the keys the file mapped, and columns ignored — headers that could not become custom fields.
  • Added to the list — confirmed memberships, or memberships awaiting confirmation on a double opt-in list.

The same import is available to your own code: POST /api/v1/contacts/import with a multipart body containing file and the optional default_status, source, skip_duplicates, run_automations (false unless you send true), tags and list_id fields, authenticated with an API key that has contacts:write (Pro or Business). The response carries imported, updated, skipped_duplicates, suppressed, skipped_unconfirmed, invalid, skipped_undeliverable, undeliverable_domains, skipped_placeholder, placeholder_addresses, tags_created, tags_attached, custom_field_keys, dropped_columns, list_added, list_pending_confirmation and truncated; see the API reference for every field. A file over 5 MB is refused with 413.

If none of the rows are usable, the import fails with a message asking for an email column header and at least one data row. SendBeam does not keep an import history, so note the summary if you need it for your records.

Stuck, or found a gap? Ask in the community — questions, tips and every release note, with this page as the source of truth.