curl --request POST \
--url https://brew.new/api/v1/contacts/import \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"contacts": [
{
"email": "ada@example.com",
"firstName": "Ada"
},
{
"email": "grace@example.com",
"firstName": "Grace",
"customFields": {
"plan": "pro"
}
}
],
"consent": {
"source": "import"
},
"fileName": "crm-export.json"
}
'import requests
url = "https://brew.new/api/v1/contacts/import"
payload = {
"contacts": [
{
"email": "ada@example.com",
"firstName": "Ada"
},
{
"email": "grace@example.com",
"firstName": "Grace",
"customFields": { "plan": "pro" }
}
],
"consent": { "source": "import" },
"fileName": "crm-export.json"
}
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({
contacts: [
{email: 'ada@example.com', firstName: 'Ada'},
{email: 'grace@example.com', firstName: 'Grace', customFields: {plan: 'pro'}}
],
consent: {source: 'import'},
fileName: 'crm-export.json'
})
};
fetch('https://brew.new/api/v1/contacts/import', 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",
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([
'contacts' => [
[
'email' => 'ada@example.com',
'firstName' => 'Ada'
],
[
'email' => 'grace@example.com',
'firstName' => 'Grace',
'customFields' => [
'plan' => 'pro'
]
]
],
'consent' => [
'source' => 'import'
],
'fileName' => 'crm-export.json'
]),
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"
payload := strings.NewReader("{\n \"contacts\": [\n {\n \"email\": \"ada@example.com\",\n \"firstName\": \"Ada\"\n },\n {\n \"email\": \"grace@example.com\",\n \"firstName\": \"Grace\",\n \"customFields\": {\n \"plan\": \"pro\"\n }\n }\n ],\n \"consent\": {\n \"source\": \"import\"\n },\n \"fileName\": \"crm-export.json\"\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")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"contacts\": [\n {\n \"email\": \"ada@example.com\",\n \"firstName\": \"Ada\"\n },\n {\n \"email\": \"grace@example.com\",\n \"firstName\": \"Grace\",\n \"customFields\": {\n \"plan\": \"pro\"\n }\n }\n ],\n \"consent\": {\n \"source\": \"import\"\n },\n \"fileName\": \"crm-export.json\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://brew.new/api/v1/contacts/import")
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 \"contacts\": [\n {\n \"email\": \"ada@example.com\",\n \"firstName\": \"Ada\"\n },\n {\n \"email\": \"grace@example.com\",\n \"firstName\": \"Grace\",\n \"customFields\": {\n \"plan\": \"pro\"\n }\n }\n ],\n \"consent\": {\n \"source\": \"import\"\n },\n \"fileName\": \"crm-export.json\"\n}"
response = http.request(request)
puts response.read_body{
"summary": {
"inserted": 2,
"updated": 0,
"failed": 0,
"skipped": 0,
"total": 2
},
"fieldsCreated": [
"plan"
],
"errors": [],
"warnings": [],
"importId": "imp_01JABCDEF0123456789XYZ"
}Bulk-import contacts from JSON
Create or update up to 1000 contacts from JSON rows and record them as one Import History batch: the JSON twin of POST /v1/contacts/import-csv. Rows take the batch-create shape (email, firstName, lastName, subscribed, customFields, consent) and follow its rules: a missing or invalid email, or an address repeated in the batch, fails its row (errors[], 207) while the others land. New contacts are subscribed unless the row says subscribed: false; re-subscribing a contact who opted out keeps its other fields and adds a RESUBSCRIBE_SKIPPED warning.
The batch is listed in Import History under fileName (optional), and each contact it writes carries the returned importId until a later import touches it (filter on it to list them; deleting the import in the app removes the contacts it created). POST /v1/contacts with contacts[] writes the same rows without an Import History entry, for recurring syncs.
Two opt-in modes. dryRun: true returns the response this import would return (summary, errors, row warnings, fieldsCreated as fields it would create) with dryRun: true and no importId, and writes nothing: no contacts, field definitions or Import History entry; validate is not run and nothing is charged. Insert vs update is decided by the same address match the write uses, so a real run right after returns the same counts. onConflict: "skip" only creates missing contacts: each existing contact is left unchanged (an opt-out row included), counted in summary.skipped with a CONTACT_EXISTS_SKIPPED warning naming its row. Either way inserted + updated + failed + skipped = total. A dry run cannot foresee failures that only happen while writing (a concurrent change, a database refusal), so the real run can still fail where the preview succeeded. With validate: true, inline checks are still metered per submitted row, rows skip left alone included; a request that leaves no address to check is not charged.
curl --request POST \
--url https://brew.new/api/v1/contacts/import \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"contacts": [
{
"email": "ada@example.com",
"firstName": "Ada"
},
{
"email": "grace@example.com",
"firstName": "Grace",
"customFields": {
"plan": "pro"
}
}
],
"consent": {
"source": "import"
},
"fileName": "crm-export.json"
}
'import requests
url = "https://brew.new/api/v1/contacts/import"
payload = {
"contacts": [
{
"email": "ada@example.com",
"firstName": "Ada"
},
{
"email": "grace@example.com",
"firstName": "Grace",
"customFields": { "plan": "pro" }
}
],
"consent": { "source": "import" },
"fileName": "crm-export.json"
}
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({
contacts: [
{email: 'ada@example.com', firstName: 'Ada'},
{email: 'grace@example.com', firstName: 'Grace', customFields: {plan: 'pro'}}
],
consent: {source: 'import'},
fileName: 'crm-export.json'
})
};
fetch('https://brew.new/api/v1/contacts/import', 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",
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([
'contacts' => [
[
'email' => 'ada@example.com',
'firstName' => 'Ada'
],
[
'email' => 'grace@example.com',
'firstName' => 'Grace',
'customFields' => [
'plan' => 'pro'
]
]
],
'consent' => [
'source' => 'import'
],
'fileName' => 'crm-export.json'
]),
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"
payload := strings.NewReader("{\n \"contacts\": [\n {\n \"email\": \"ada@example.com\",\n \"firstName\": \"Ada\"\n },\n {\n \"email\": \"grace@example.com\",\n \"firstName\": \"Grace\",\n \"customFields\": {\n \"plan\": \"pro\"\n }\n }\n ],\n \"consent\": {\n \"source\": \"import\"\n },\n \"fileName\": \"crm-export.json\"\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")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"contacts\": [\n {\n \"email\": \"ada@example.com\",\n \"firstName\": \"Ada\"\n },\n {\n \"email\": \"grace@example.com\",\n \"firstName\": \"Grace\",\n \"customFields\": {\n \"plan\": \"pro\"\n }\n }\n ],\n \"consent\": {\n \"source\": \"import\"\n },\n \"fileName\": \"crm-export.json\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://brew.new/api/v1/contacts/import")
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 \"contacts\": [\n {\n \"email\": \"ada@example.com\",\n \"firstName\": \"Ada\"\n },\n {\n \"email\": \"grace@example.com\",\n \"firstName\": \"Grace\",\n \"customFields\": {\n \"plan\": \"pro\"\n }\n }\n ],\n \"consent\": {\n \"source\": \"import\"\n },\n \"fileName\": \"crm-export.json\"\n}"
response = http.request(request)
puts response.read_body{
"summary": {
"inserted": 2,
"updated": 0,
"failed": 0,
"skipped": 0,
"total": 2
},
"fieldsCreated": [
"plan"
],
"errors": [],
"warnings": [],
"importId": "imp_01JABCDEF0123456789XYZ"
}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 - 1000 elementsShow child attributes
Show child attributes
Optional deliverability check on ingestion. When true, each imported address is validated with the provider and the verdict is saved to the contact’s validationStatus. Imports of at most 100 submitted contacts validate inline, metered at 2 credits per submitted contact on a successful response, including rows that fail. Larger imports write first and use a background validation workflow billed per address as it runs; validationJobId is returned when a job is queued.
Default consent record for every row that does not carry its own consent (typically { "source": "import" }).
Show child attributes
Show child attributes
Name this batch is listed under in Import History (usually the source file name, e.g. customers.csv), also stamped on every contact it writes as csvFileName. A path keeps only its last segment.
1 - 255When true, returns what this import would return and writes nothing: no contacts, fields, Import History entry or importId; validate is skipped and nothing is charged.
upsert (default) updates existing contacts; skip only creates missing ones, leaving each existing contact unchanged (counted in summary.skipped, with a CONTACT_EXISTS_SKIPPED warning).
upsert, skip Response
Import summary (all rows valid).
Show child attributes
Show child attributes
Show child attributes
Show child attributes
Show child attributes
Show child attributes
Show child attributes
Show child attributes
This batch's Import History id. Each contact it wrote carries it as importId until a later import touches that contact, so a contact filter on importId lists them. Absent on a dry run.
Present on a dry run: the counts say what the import would do, and nothing was written.
true Was this page helpful?