curl --request POST \
--url https://brew.new/api/v1/contacts/import-csv \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"csv": "email,firstName,lastName\nada@example.com,Ada,Lovelace\ngrace@example.com,Grace,Hopper",
"mapping": {
"email": "email",
"firstName": "firstName",
"lastName": "lastName"
}
}
'import requests
url = "https://brew.new/api/v1/contacts/import-csv"
payload = {
"csv": "email,firstName,lastName
ada@example.com,Ada,Lovelace
grace@example.com,Grace,Hopper",
"mapping": {
"email": "email",
"firstName": "firstName",
"lastName": "lastName"
}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
csv: 'email,firstName,lastName\nada@example.com,Ada,Lovelace\ngrace@example.com,Grace,Hopper',
mapping: {email: 'email', firstName: 'firstName', lastName: 'lastName'}
})
};
fetch('https://brew.new/api/v1/contacts/import-csv', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://brew.new/api/v1/contacts/import-csv",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'csv' => 'email,firstName,lastName
ada@example.com,Ada,Lovelace
grace@example.com,Grace,Hopper',
'mapping' => [
'email' => 'email',
'firstName' => 'firstName',
'lastName' => 'lastName'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://brew.new/api/v1/contacts/import-csv"
payload := strings.NewReader("{\n \"csv\": \"email,firstName,lastName\\nada@example.com,Ada,Lovelace\\ngrace@example.com,Grace,Hopper\",\n \"mapping\": {\n \"email\": \"email\",\n \"firstName\": \"firstName\",\n \"lastName\": \"lastName\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://brew.new/api/v1/contacts/import-csv")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"csv\": \"email,firstName,lastName\\nada@example.com,Ada,Lovelace\\ngrace@example.com,Grace,Hopper\",\n \"mapping\": {\n \"email\": \"email\",\n \"firstName\": \"firstName\",\n \"lastName\": \"lastName\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://brew.new/api/v1/contacts/import-csv")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"csv\": \"email,firstName,lastName\\nada@example.com,Ada,Lovelace\\ngrace@example.com,Grace,Hopper\",\n \"mapping\": {\n \"email\": \"email\",\n \"firstName\": \"firstName\",\n \"lastName\": \"lastName\"\n }\n}"
response = http.request(request)
puts response.read_body{
"summary": {
"inserted": 2,
"updated": 0,
"failed": 0,
"skipped": 0
},
"fieldsCreated": [],
"errors": [],
"warnings": []
}Bulk-import contacts from CSV
Parse a raw CSV string and upsert the rows as contacts (≤ 1000). By default each column header maps to a field of the same name (email is required). A header that spells a core field, ignoring case, spaces, _ and -, or whose field key equals it (First.Name), maps to it: Email, First Name, Last Name and Subscribed fill email, firstName, lastName and subscribed. Pass mapping to remap single columns; the columns it leaves out keep their default mapping, "" leaves a column out, and a key that names no column is a 400. A core field that mapping fills is filled from that column alone: another column whose default would fill it is left out with a CSV_COLUMN_IGNORED warning. Rows with a missing/invalid email are SKIPPED (counted in summary.skipped), not errored; disposable-domain rows are imported with validationStatus: "risky" (deliverable but flagged). Returns the same { summary, fieldsCreated, errors, warnings } as batch-create — 207 when some rows fail.
CSV cells are text: a column mapped onto an EXISTING field is coerced to that field’s type (dates → epoch ms, 1,234 → 1234, yes/no → booleans), and a cell that cannot be coerced ($49 in a number column) fails ITS row with a per-row errors[] entry (code: FIELD_TYPE_MISMATCH, field) while the other rows still land. An undeclared column is created as a string field unless every value is an ISO date (→ date); predeclare number / bool columns with POST /v1/fields. A column whose field Brew manages (Created At, Validation Status) is not imported and adds a warnings[] entry (code: CSV_COLUMN_IGNORED, field names the column).
A date column is read in ONE day/month order: the order its own dates prove (a day over 12 anywhere in the column, 13/04/2026), else dateOrder (day_first reads 03/04/2026 as 3 April, month_first as 4 March), else month/day with a warnings[] entry (code: DATE_ORDER_ASSUMED, field). Dotted dates (03.04.2026) are day-first. ISO (2026-04-03), Unix seconds or milliseconds, month names (3 Apr 2026), year-first (2026/04/03) and compact (20260403) dates need no order. Convert to YYYY-MM-DD to be unambiguous.
A subscribed column reads true/false and ESP status words (unsubscribed, cleaned, blocklisted, transactional, active, …). An opt-out unsubscribes the contact, new or existing. A word that is not a subscription status (maybe) fails its row (code: FIELD_TYPE_MISMATCH, field: subscribed) instead of importing the contact subscribed. A subscribed value only applies to new contacts: a row that asks to re-subscribe a contact who opted out keeps its other fields and adds a warnings[] entry (code: RESUBSCRIBE_SKIPPED, email).
curl --request POST \
--url https://brew.new/api/v1/contacts/import-csv \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"csv": "email,firstName,lastName\nada@example.com,Ada,Lovelace\ngrace@example.com,Grace,Hopper",
"mapping": {
"email": "email",
"firstName": "firstName",
"lastName": "lastName"
}
}
'import requests
url = "https://brew.new/api/v1/contacts/import-csv"
payload = {
"csv": "email,firstName,lastName
ada@example.com,Ada,Lovelace
grace@example.com,Grace,Hopper",
"mapping": {
"email": "email",
"firstName": "firstName",
"lastName": "lastName"
}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
csv: 'email,firstName,lastName\nada@example.com,Ada,Lovelace\ngrace@example.com,Grace,Hopper',
mapping: {email: 'email', firstName: 'firstName', lastName: 'lastName'}
})
};
fetch('https://brew.new/api/v1/contacts/import-csv', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://brew.new/api/v1/contacts/import-csv",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'csv' => 'email,firstName,lastName
ada@example.com,Ada,Lovelace
grace@example.com,Grace,Hopper',
'mapping' => [
'email' => 'email',
'firstName' => 'firstName',
'lastName' => 'lastName'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://brew.new/api/v1/contacts/import-csv"
payload := strings.NewReader("{\n \"csv\": \"email,firstName,lastName\\nada@example.com,Ada,Lovelace\\ngrace@example.com,Grace,Hopper\",\n \"mapping\": {\n \"email\": \"email\",\n \"firstName\": \"firstName\",\n \"lastName\": \"lastName\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://brew.new/api/v1/contacts/import-csv")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"csv\": \"email,firstName,lastName\\nada@example.com,Ada,Lovelace\\ngrace@example.com,Grace,Hopper\",\n \"mapping\": {\n \"email\": \"email\",\n \"firstName\": \"firstName\",\n \"lastName\": \"lastName\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://brew.new/api/v1/contacts/import-csv")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"csv\": \"email,firstName,lastName\\nada@example.com,Ada,Lovelace\\ngrace@example.com,Grace,Hopper\",\n \"mapping\": {\n \"email\": \"email\",\n \"firstName\": \"firstName\",\n \"lastName\": \"lastName\"\n }\n}"
response = http.request(request)
puts response.read_body{
"summary": {
"inserted": 2,
"updated": 0,
"failed": 0,
"skipped": 0
},
"fieldsCreated": [],
"errors": [],
"warnings": []
}Authorizations
Send your Brew API key as Authorization: Bearer brew_xxx.
Headers
The brand this request acts on. REQUIRED for organization-scoped credentials (otherwise 400 BRAND_ID_REQUIRED — there is no default brand); list ids with GET /v1/brands. Brand-scoped credentials may omit it, and sending a different brand returns 403 BRAND_SCOPE_MISMATCH. A brand outside your organization returns 404 BRAND_NOT_FOUND.
1 - 64Body
1 - 5000000Show child attributes
Show child attributes
Optional deliverability check on ingestion. When true, each address is validated with the provider (2 credits per address, charged on success) and the verdict is saved to the contact’s validationStatus. Submissions above the inline cap (100 addresses) upsert first and validate as a background job, returning a validationJobId.
Consent record stamped on every imported row (typically { "source": "import" }).
Show child attributes
Show child attributes
Order for a date column whose dates read either way (03/04/2026): day_first is 3 April, month_first 4 March. A day over 12 in the column overrides it. Default month-first, with a DATE_ORDER_ASSUMED warning.
month_first, day_first Was this page helpful?