스크립트로 CSV 또는 Excel 연락처를 Brevo에 가져오는 방법 (Python, Node.js, cURL)

import-contacts API를 사용하여 CSV 또는 Excel 파일에서 Brevo로 연락처를 일괄 가져옵니다. 바로 실행할 수 있는 Python 및 Node.js 스크립트, cURL 한 줄 명령, 오류 처리, 10MB를 초과하는 파일에 대한 팁이 포함되어 있습니다.

Featured image for article: 스크립트로 CSV 또는 Excel 연락처를 Brevo에 가져오는 방법 (Python, Node.js, cURL)

스프레드시트에서 수천 개의 연락처를 Brevo로 푸시해야 했던 경험이 있다면, 수동 UI 경로가 빠르게 고통스러워진다는 것을 아실 것입니다. 파일 선택, 열 매핑, 리스트 선택, 대기, 반복. 스크립트는 같은 일을 몇 초 만에 처리합니다. 더 중요한 것은 스케줄에 맞춰, 데이터베이스 익스포트 후, 또는 CRM이 새로운 CSV를 내놓을 때마다 다시 실행할 수 있다는 점입니다.

이 가이드는 정확히 이 목적을 위해 Brevo가 제공하는 API 엔드포인트를 다루며, Python, Node.js의 완전히 작동하는 스크립트와 cURL 한 줄 명령을 함께 제공합니다. 이 가이드의 모든 것은 POST /v3/contacts/import 에 접근합니다.

엔드포인트 한눈에 보기

POST https://api.brevo.com/v3/contacts/import
Content-Type: application/json
api-key: YOUR_API_KEY
{
"fileBody": "EMAIL;FIRSTNAME;LASTNAME\n[email protected];Jane;Doe",
"listIds": [42],
"updateExistingContacts": true,
"emailBlacklist": false,
"smsBlacklist": false
}

응답은 빠르게 돌아옵니다.

{ "processId": 78 }

이것은 202 Accepted입니다. Brevo가 가져오기를 수락하고 백그라운드에서 처리하고 있습니다. processId는 완료를 폴링하거나 notifyUrl 웹훅을 설정하려는 경우의 추적 핸들입니다.

코드를 작성하기 전에 알아둘 사항이 몇 가지 있습니다.

  • 본문은 CSV(fileBody), JSON(jsonBody), 또는 원격 URL(fileUrl)이 될 수 있습니다. 하나를 선택하세요. CSV 형식은 열을 분리하기 위해 쉼표가 아닌 세미콜론(;)을 사용합니다. 흔한 함정입니다.
  • fileBodyjsonBody는 10MB로 제한됩니다. Brevo는 안전을 위해 8MB 정도로 유지하는 것을 권장합니다. 더 큰 경우, 파일을 S3, GCS 또는 HTTPS 호스트에 업로드하고 대신 fileUrl을 전달하세요.
  • 계정에 존재하지 않는 사용자 정의 속성은 자동으로 무시됩니다. 사용하는 행을 가져오기 전에 Brevo의 UI에서 (또는 Attributes API를 통해) 만들어두세요. 그렇지 않으면 데이터가 그냥 사라집니다.
  • updateExistingContacts는 기본값이 true입니다. false로 설정하면, Brevo는 이메일이 이미 존재하는 연락처를 건너뜁니다. “신규만 추가” 작업에 유용합니다.
  • emptyContactsAttributes 는 CSV의 빈 셀이 기존 값을 지울지 여부를 제어합니다. 기본값은 false(빈 셀이 무시됨)입니다. CSV가 진실의 원천이고 빈 칸이 오래된 데이터를 지우도록 하려면 true로 설정하세요.

API 키 가져오기

Brevo에 로그인하고 Settings → SMTP & API → API Keys 로 이동하여 새 키를 만듭니다. xkeysib-... 같은 형식입니다. 모든 요청에서 api-key HTTP 헤더로 전달합니다. 비밀번호처럼 다루세요. 계정에 대한 전체 읽기/쓰기 권한이 있습니다.

깔끔한 패턴: 소스 트리에 들어가지 않도록 환경 변수에 저장합니다.

Terminal window
export BREVO_API_KEY="xkeysib-..."

Python: CSV를 읽어 Brevo로 푸시

가장 간단한 스크립트입니다. 표준 라이브러리로 로컬 CSV를 읽고, 본문을 바로 Brevo로 보냅니다. requests 외에는 서드파티 SDK가 필요하지 않습니다.

import_csv_to_brevo.py
import csv
import io
import os
import sys
import requests
API_KEY = os.environ["BREVO_API_KEY"]
LIST_ID = 42 # the Brevo list to add contacts to
INPUT_FILE = sys.argv[1] if len(sys.argv) > 1 else "contacts.csv"
# Brevo wants semicolon-separated CSV. If your file uses commas (most do),
# convert it on the fly so you don't have to edit the source spreadsheet.
def to_brevo_csv(path: str) -> str:
with open(path, newline="", encoding="utf-8") as f:
reader = csv.reader(f) # default: comma-separated
out = io.StringIO()
writer = csv.writer(out, delimiter=";")
for row in reader:
writer.writerow(row)
return out.getvalue()
body = {
"fileBody": to_brevo_csv(INPUT_FILE),
"listIds": [LIST_ID],
"updateExistingContacts": True,
"emptyContactsAttributes": False,
"emailBlacklist": False,
"smsBlacklist": False,
}
resp = requests.post(
"https://api.brevo.com/v3/contacts/import",
json=body,
headers={"api-key": API_KEY, "Content-Type": "application/json"},
timeout=60,
)
if resp.status_code != 202:
print(f"Import failed: {resp.status_code} {resp.text}")
sys.exit(1)
process_id = resp.json()["processId"]
print(f"Import accepted. Process ID: {process_id}")

실행:

Terminal window
python import_csv_to_brevo.py contacts.csv

CSV의 첫 번째 행은 헤더입니다. 열 이름은 대문자, 정확히 일치 로 Brevo 속성에 매핑됩니다. EMAIL, FIRSTNAME, LASTNAME 그리고 정의한 모든 사용자 정의 속성입니다. EMAIL은 필수입니다.

EMAIL,FIRSTNAME,LASTNAME,COMPANY,CITY
[email protected],Jane,Doe,Acme,Berlin
[email protected],John,Smith,Globex,Paris

이것이 전체 흐름입니다. 스크립트는 1초 이내에 반환되고, 실제 가져오기는 서버 측에서 실행되며 볼륨에 따라 몇 초에서 몇 분이 걸립니다.

Excel 파일: 실용적인 세 가지 경로

Brevo의 가져오기 엔드포인트는 .xlsx를 직접 읽지 않으므로, 작업이 어디에 있어야 하는지에 따라 세 가지 실제 옵션이 있습니다.

  1. 워크북 내에서 VBA 매크로 실행. 외부 스크립트 없이 Excel 자체에서 원클릭 동기화. 파일이 데스크톱에 있고 팀이 시트에 버튼을 원하는 경우 정답입니다. 전체 코드는 Excel-to-Brevo VBA 매크로 가이드에 있습니다.
  2. Office Scripts + Power Automate. 워크북이 OneDrive/SharePoint에 있고 무인 스케줄 동기화를 원할 때. Excel 가이드에서도 다룹니다.
  3. 스크립트에서 .xlsx를 CSV로 변환. 이 섹션의 나머지 부분이 다루는 내용입니다. 이미 서버에서 Python이나 Node를 실행 중이고 하루에 한 번 워크북을 가져오기만 하면 되는 경우 가장 좋습니다.

옵션 3의 경우, pandas로 한 줄이 됩니다.

# pip install pandas openpyxl
import pandas as pd
def xlsx_to_brevo_csv(path: str, sheet_name: str | int = 0) -> str:
df = pd.read_excel(path, sheet_name=sheet_name)
return df.to_csv(sep=";", index=False)
body["fileBody"] = xlsx_to_brevo_csv("contacts.xlsx")

pandas를 의존성으로 원하지 않는다면, openpyxl만으로도 작동합니다.

from openpyxl import load_workbook
import csv, io
def xlsx_to_brevo_csv(path: str) -> str:
wb = load_workbook(path, read_only=True)
ws = wb.active
out = io.StringIO()
writer = csv.writer(out, delimiter=";")
for row in ws.iter_rows(values_only=True):
writer.writerow(["" if v is None else v for v in row])
return out.getvalue()

Node.js: 같은 작업, 공식 SDK

Brevo는 Node용으로 @getbrevo/brevo를 게시합니다. 인증, 재시도, 타입이 지정된 요청 모양을 처리합니다.

import-csv-to-brevo.mjs
// npm install @getbrevo/brevo
import fs from 'node:fs/promises';
import { BrevoClient } from '@getbrevo/brevo';
const API_KEY = process.env.BREVO_API_KEY;
const LIST_ID = 42;
const INPUT_FILE = process.argv[2] ?? 'contacts.csv';
const client = new BrevoClient({ apiKey: API_KEY });
// Read the CSV and convert commas to semicolons if needed
const raw = await fs.readFile(INPUT_FILE, 'utf-8');
const csv = raw.includes(';') ? raw : raw.replace(/,/g, ';');
const result = await client.contacts.importContacts({
fileBody: csv,
listIds: [LIST_ID],
updateExistingContacts: true,
emptyContactsAttributes: false,
});
console.log(`Import accepted. Process ID: ${result.processId}`);

또는 SDK를 원하지 않으면, 일반 fetch로도 같은 방식으로 작동합니다.

const resp = await fetch('https://api.brevo.com/v3/contacts/import', {
method: 'POST',
headers: {
'api-key': process.env.BREVO_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
fileBody: csv,
listIds: [42],
updateExistingContacts: true,
}),
});
if (resp.status !== 202) {
throw new Error(`Import failed: ${resp.status} ${await resp.text()}`);
}
const { processId } = await resp.json();
console.log(`Process ID: ${processId}`);

cURL: 임시 가져오기를 위한 한 줄 명령

엔드포인트를 테스트하거나 작은 파일을 손으로 밀어 넣고 싶을 때:

Terminal window
# Convert commas to semicolons, then send the file inline
csv=$(sed 's/,/;/g' contacts.csv | jq -Rs .)
curl -X POST https://api.brevo.com/v3/contacts/import \
-H "api-key: $BREVO_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"fileBody\": $csv, \"listIds\": [42], \"updateExistingContacts\": true}"

jq -Rs .는 전체 파일을 슬러프하고 JSON 이스케이프합니다. 줄바꿈과 따옴표를 수동으로 이스케이프하지 않아도 됩니다.

10MB를 초과하는 파일: fileUrl 사용

fileBody는 10MB로 제한됩니다. 연락처 덤프가 더 크다면, Brevo가 가져올 수 있는 URL에 파일을 호스팅하고 그것을 전달하세요.

body = {
"fileUrl": "https://files.example.com/contacts/2026-04-30.csv",
"listIds": [42],
"updateExistingContacts": True,
}

공개 HTTPS URL로 도달 가능한 모든 것이 작동합니다. 사전 서명된 S3 URL, GCS, 자체 정적 호스트, 일회성 가져오기를 위한 GitHub raw URL까지. Brevo는 사용자의 URL에서 파일을 가져온 다음 가져오기를 실행합니다. 허용된 형식: .csv, .txt, .json.

CSV 대신 JSON 보내기

이미 데이터베이스에서 연락처를 가져오고 있다면, CSV를 통해 갈 필요가 없습니다. JSON으로 직접 보내세요.

body = {
"jsonBody": [
{
"email": "[email protected]",
"attributes": {
"FIRSTNAME": "Jane",
"LASTNAME": "Doe",
"COMPANY": "Acme",
},
},
{
"email": "[email protected]",
"attributes": {
"FIRSTNAME": "John",
"LASTNAME": "Smith",
"COMPANY": "Globex",
},
},
],
"listIds": [42],
}

같은 10MB 제한. 같은 비동기 동작.

완료 폴링

가져오기는 비동기적입니다. processId는 추적할 핸들입니다. 프로세스 상태를 확인할 별도의 엔드포인트가 있습니다.

def wait_for_import(process_id: int, timeout_s: int = 600) -> dict:
import time
deadline = time.time() + timeout_s
while time.time() < deadline:
r = requests.get(
f"https://api.brevo.com/v3/processes/{process_id}",
headers={"api-key": API_KEY},
timeout=30,
)
r.raise_for_status()
status = r.json()
if status["status"] in ("completed", "failed"):
return status
time.sleep(5)
raise TimeoutError(f"Import {process_id} did not finish in {timeout_s}s")
result = wait_for_import(process_id)
print(f"Import {result['status']}: {result}")

장기 실행 작업의 경우, 더 깔끔한 패턴은 notifyUrl입니다. 가져오기가 완료되면 Brevo가 POST할 HTTPS 엔드포인트를 전달하면 폴링을 건너뛸 수 있습니다.

body["notifyUrl"] = "https://your-app.example.com/webhooks/brevo-import"

일반적인 오류와 해결 방법

명백한 원인 없는 400 Bad Request. 거의 항상 CSV의 세미콜론 대 쉼표 문제입니다. Brevo의 가져오기는 ,가 아닌 ;를 기대합니다. 변환 단계 후의 fileBody를 다시 확인하세요.

사용자 정의 속성 값이 사라집니다. 그 속성이 계정에 존재하지 않습니다. 가져오기 전에 Contacts → Settings → Contact attributes 아래에서 만들거나, 스크립트의 일부로 만들기 위해 Attributes API를 사용하세요.

401 Unauthorized. 잘못된 헤더 이름. Authorization이나 X-API-Key가 아니라 api-key(소문자, 하이픈)입니다.

가져오기는 “성공”했지만 연락처가 리스트에 나타나지 않습니다. listIds에 올바른 리스트가 포함되어 있는지 확인하세요. 또한 updateExistingContactsfalse이고 연락처가 이미 존재하면, Brevo는 리스트에 다시 추가하지 않고 자동으로 건너뜁니다.

일부 행은 가져왔고 일부는 가져오지 못했습니다. Brevo는 가져오기가 완료된 후 행별 오류 보고서를 이메일로 보냅니다(disableNotification: true로 설정하지 않은 경우). 보고서는 잘못된 이메일, 누락된 필수 필드, 또는 형식 문제가 있는 행을 알려줍니다.

스크립트 vs UI 사용 시기

UI는 1,000행 미만의 일회성 가져오기에 적합합니다. 다음의 경우 스크립트가 이깁니다.

  • 한 번 이상 가져오는 경우(예: CRM에서 주간 익스포트)
  • Brevo에 도착하기 전에 소스 데이터를 정리해야 하는 경우(중복 제거, 전화번호 형식 지정, 전체 이름을 성/이름으로 분할)
  • 누구도 버튼을 클릭하지 않고 스케줄에 맞춰 실행되기를 원하는 경우
  • 파일이 UI의 편안한 영역보다 큰 경우

위의 스크립트를 cron 작업이나 GitHub Action으로 감싸면 자동 연락처 동기화가 됩니다. 이 시리즈의 다음 게시물에서는 Apps Script를 사용하여 Google Sheet에서 직접 같은 작업을 수행하는 방법을 보여줍니다. 서버가 필요 없습니다.

더 읽기

Frequently Asked Questions

연락처를 일괄 가져오기 위한 Brevo API 엔드포인트는 무엇입니까?
POST https://api.brevo.com/v3/contacts/import 입니다. CSV 콘텐츠(fileBody), 원격 파일 URL(fileUrl), 또는 JSON 배열(jsonBody)을 받습니다. 엔드포인트는 비동기적이며 즉시 processId를 반환하고 백그라운드에서 가져오기를 완료합니다.
최대 파일 크기는 얼마입니까?
인라인 CSV(fileBody) 또는 JSON(jsonBody)의 경우 10MB입니다. 더 큰 파일의 경우, 도달 가능한 곳에 파일을 호스팅하고 fileUrl을 통해 URL을 전달하세요. 이 경로에는 문서화된 크기 제한이 없습니다.
.xlsx 파일은 어떻게 가져옵니까?
Brevo의 가져오기 API는 .csv, .txt, .json만 받습니다. 먼저 .xlsx를 .csv로 변환하세요. pandas, openpyxl, 또는 LibreOffice headless가 한 줄로 처리할 수 있습니다. 이 가이드의 스크립트에는 xlsx에서 csv로의 변환이 포함되어 있습니다.
기존 연락처를 덮어쓸까요?
기본적으로 그렇습니다. updateExistingContacts는 기본값이 true이며 이메일로 일치시킵니다. false로 설정하면 이미 존재하는 연락처를 건너뜁니다. 빈 CSV 셀이 기존 값을 지우길 원하면 emptyContactsAttributes를 true로 설정하세요(그렇지 않으면 빈 셀은 무시됩니다).
Brevo로 무료로 시작하기