curl --request POST \
--url https://brew.new/api/v1/contacts \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"email": "jane@example.com",
"firstName": "Jane",
"customFields": {
"plan": "enterprise"
},
"consent": {
"source": "form",
"capturedAt": "2026-04-08T12:00:00.000Z",
"policyVersion": "2026-03",
"evidence": "newsletter checkbox on /signup"
}
}
'{
"summary": {
"inserted": 2,
"updated": 0,
"failed": 0
},
"fieldsCreated": [],
"errors": [],
"warnings": []
}Create or update contacts
Upserts a single contact OR a batch ({ contacts: [...] }, up to 1000 rows). Unknown custom fields auto-create field definitions on the brand, typed from the batch (native booleans/numbers, ISO-date strings → date, anything else string).
Custom-field values are coerced to the definition’s type (dates → epoch ms, "1,234" → 1234, yes/no → booleans). A value that cannot be coerced ("$49" in a number field) is a 409 FIELD_TYPE_MISMATCH on a single upsert and a per-row errors[] entry (code: FIELD_TYPE_MISMATCH, field) in a batch — the other rows still land. A customFields key is never stored under a core field name. One whose loss would change the contact (subscribed, First Name, consent, a suppression flag or domain opt-out, or an email naming another address) is a 422 CORE_FIELD_IMMUTABLE on a single upsert (nothing written) and a per-row errors[] entry (code: CORE_FIELD_IMMUTABLE, field) in a batch; send firstName, lastName, subscribed and consent at the top level. A key naming a field Brew manages (createdAt, validationStatus) is dropped with a warnings[] entry (code: CORE_FIELD_IGNORED, field) and the contact is still written, and an email key repeating the contact address is ignored.
Dates: a JSON number is epoch milliseconds. A string may be ISO (2026-04-03, with or without a time and offset), a Unix timestamp in seconds (10 digits) or milliseconds (13), a month name (3 Apr 2026, April 3, 2026, 03-Apr-26), year-first (2026/04/03), compact (20260403), month and year read as the 1st (2026-04), dotted day-first (03.04.2026), or a slash or dash date. A slash or dash date with a day over 12 reads that way (13/04/2026 is 13 April). One that reads either way (03/04/2026) follows the day/month order the batch’s other dates in that field prove, else month/day, with a warnings[] entry (code: DATE_ORDER_ASSUMED, field). Send YYYY-MM-DD to be unambiguous.
Optional consent: { source: "api" | "form" | "import", capturedAt?, policyVersion?, evidence? } records marketing consent provenance on the contact (per row, or once at batch level as the default). It never changes subscribed. An inline marketing send later needs the contact to be subscribed, and warns when no record exists.
subscribed: false unsubscribes the contact, new or existing. subscribed: true only applies to a NEW contact: Brew never re-subscribes a contact who opted out, so a single upsert asking for it is a 422 RESUBSCRIBE_NOT_ALLOWED (nothing written), and a batch row keeps its other fields and adds a warnings[] entry (code: RESUBSCRIBE_SKIPPED, email).
Single: 201 with { contact, created, fieldsCreated, warnings }. Batch: 200 with { summary, fieldsCreated, errors, warnings } — or 207 when some rows failed (per-row errors in errors[]).
curl --request POST \
--url https://brew.new/api/v1/contacts \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"email": "jane@example.com",
"firstName": "Jane",
"customFields": {
"plan": "enterprise"
},
"consent": {
"source": "form",
"capturedAt": "2026-04-08T12:00:00.000Z",
"policyVersion": "2026-03",
"evidence": "newsletter checkbox on /signup"
}
}
'{
"summary": {
"inserted": 2,
"updated": 0,
"failed": 0
},
"fieldsCreated": [],
"errors": [],
"warnings": []
}Authorizations
Send your Brew API key as Authorization: Bearer brew_xxx.
Headers
Optional idempotency key for safe retries. Reusing the same key with the same request body returns the original response for 24 hours.
1 - 100The 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
- Option 1
- Option 2
1Show child attributes
Show child attributes
Marketing consent provenance to record on this contact. Optional at creation; an inline marketing send to this address later needs the contact to be subscribed and warns when no record exists.
Show 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.
Was this page helpful?