API Reference
The BandTools API is designed to be predictable, explicit and safe to use in production. It is organised around REST, with resource-oriented URLs, JSON-encoded request and response bodies, and standard HTTP verbs, authentication and status codes. File uploads use multipart form data.
The API is available on all plans. Some endpoints enforce plan-level gates (for example, scheduling a newsletter requires the Headliner plan), but access to the API itself is not gated.
The current version is v1, served under the /api/v1 path prefix.
An OpenAPI 3.0 schema is available for this API. Import it into Postman, Insomnia, Stoplight, openapi-generator, or any other tool that speaks OpenAPI to generate clients, request collections, or mock servers.
11.8 Send to new subscribers
curl -X POST "$API/newsletters/$NEWSLETTER_SLUG/send-to-new-subscribers" \ -H "Authorization: Bearer $BANDTOOLS_API_TOKEN"
import os, requests response = requests.post( f"{os.environ['API']}/newsletters/{os.environ['NEWSLETTER_SLUG']}/send-to-new-subscribers", headers={"Authorization": f"Bearer {os.environ['BANDTOOLS_API_TOKEN']}"}, ) response.raise_for_status() print(response.json())
const response = await fetch( `${process.env.API}/newsletters/${process.env.NEWSLETTER_SLUG}/send-to-new-subscribers`, { method: "POST", headers: { Authorization: `Bearer ${process.env.BANDTOOLS_API_TOKEN}` } } ); console.log(await response.json());
req, _ := http.NewRequest("POST", os.Getenv("API")+"/newsletters/"+os.Getenv("NEWSLETTER_SLUG")+"/send-to-new-subscribers", nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("BANDTOOLS_API_TOKEN")) response, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer response.Body.Close()
require "net/http" uri = URI("#{ENV.fetch("API")}/newsletters/#{ENV.fetch("NEWSLETTER_SLUG")}/send-to-new-subscribers") request = Net::HTTP::Post.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BANDTOOLS_API_TOKEN")}" Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }
HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(System.getenv("API") + "/newsletters/" + System.getenv("NEWSLETTER_SLUG") + "/send-to-new-subscribers")) .header("Authorization", "Bearer " + System.getenv("BANDTOOLS_API_TOKEN")) .POST(HttpRequest.BodyPublishers.noBody()) .build(); HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.discarding());
$ch = curl_init(getenv("API") . "/newsletters/" . getenv("NEWSLETTER_SLUG") . "/send-to-new-subscribers"); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer " . getenv("BANDTOOLS_API_TOKEN"), ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_exec($ch); curl_close($ch);
11.10 Cancel a scheduled newsletter
curl -X DELETE "$API/newsletters/$NEWSLETTER_SLUG/schedule" \ -H "Authorization: Bearer $BANDTOOLS_API_TOKEN"
import os, requests response = requests.delete( f"{os.environ['API']}/newsletters/{os.environ['NEWSLETTER_SLUG']}/schedule", headers={"Authorization": f"Bearer {os.environ['BANDTOOLS_API_TOKEN']}"}, ) response.raise_for_status()
await fetch( `${process.env.API}/newsletters/${process.env.NEWSLETTER_SLUG}/schedule`, { method: "DELETE", headers: { Authorization: `Bearer ${process.env.BANDTOOLS_API_TOKEN}` } } );
req, _ := http.NewRequest("DELETE", os.Getenv("API")+"/newsletters/"+os.Getenv("NEWSLETTER_SLUG")+"/schedule", nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("BANDTOOLS_API_TOKEN")) response, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } response.Body.Close()
require "net/http" uri = URI("#{ENV.fetch("API")}/newsletters/#{ENV.fetch("NEWSLETTER_SLUG")}/schedule") request = Net::HTTP::Delete.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BANDTOOLS_API_TOKEN")}" Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }
HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(System.getenv("API") + "/newsletters/" + System.getenv("NEWSLETTER_SLUG") + "/schedule")) .header("Authorization", "Bearer " + System.getenv("BANDTOOLS_API_TOKEN")) .DELETE() .build(); HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.discarding());
$ch = curl_init(getenv("API") . "/newsletters/" . getenv("NEWSLETTER_SLUG") . "/schedule"); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer " . getenv("BANDTOOLS_API_TOKEN"), ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_exec($ch); curl_close($ch);
12.1 List collaborators
curl -H "Authorization: Bearer $BANDTOOLS_API_TOKEN" \ "$API/newsletters/$NEWSLETTER_SLUG/collaborators?sort=name_asc"
Response:
{ "data": [ { "id": 1, "email_address": "collaborator@example.com", "status": "accepted", "name": "Jane Doe", "invited_at": "2026-05-01T12:00:00Z", "accepted_at": "2026-05-01T14:00:00Z", "last_edited_at": "2026-05-02T09:30:00Z" }, { "id": 2, "email_address": "pending@example.com", "status": "pending", "name": null, "invited_at": "2026-05-03T10:00:00Z", "accepted_at": null, "last_edited_at": null } ], "meta": { "request_id": "..." } }
12.2 Invite a collaborator
403 plan_feature_required.curl -X POST "$API/newsletters/$NEWSLETTER_SLUG/collaborators" \ -H "Authorization: Bearer $BANDTOOLS_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"email_address": "collaborator@example.com"}'
Returns 201 Created with the new collaborator object. Errors:
409 conflictwithcollaborator_limit_reachedwhen the newsletter already has 10 collaborators409 conflictwhen the newsletter has already been sent422 validation_failedwhen the email address is invalid, already invited, or is the owner’s own address403 plan_feature_requiredwhen the plan does not include collaborative editing
12.3 Revoke a collaborator
curl -X DELETE "$API/newsletters/$NEWSLETTER_SLUG/collaborators/1" \ -H "Authorization: Bearer $BANDTOOLS_API_TOKEN"
Returns 204 No Content.
12.5 Acquire the editing lock
curl -X POST "$API/newsletters/$NEWSLETTER_SLUG/lock" \ -H "Authorization: Bearer $BANDTOOLS_API_TOKEN"
Returns 200 with { "data": { "acquired": true, "lock_holder": "...", "locked_until": "..." } } on success.
Returns 409 conflict when another user currently holds the lock.
12.6 Refresh the editing lock
curl -X PATCH "$API/newsletters/$NEWSLETTER_SLUG/lock/heartbeat" \ -H "Authorization: Bearer $BANDTOOLS_API_TOKEN"
Returns 409 conflict if you do not hold the lock.
12.7 Release the editing lock
curl -X DELETE "$API/newsletters/$NEWSLETTER_SLUG/lock" \ -H "Authorization: Bearer $BANDTOOLS_API_TOKEN"
Returns 204 No Content.
14.8 Validate a feed URL
Successful response shape:
{ "data": { "valid": true, "feed_title": "Studio Diary", "site_url": "https://example.com", "latest_item_title": "Mixing day three", "latest_item_url": "https://example.com/posts/mixing-day-three" }, "meta": { "request_id": "..." } }
curl -X POST "$API/automatic-newsletters/validate" \ -H "Authorization: Bearer $BANDTOOLS_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"feed_url":"https://example.com/diary.xml"}'
import os, requests response = requests.post( f"{os.environ['API']}/automatic-newsletters/validate", headers={"Authorization": f"Bearer {os.environ['BANDTOOLS_API_TOKEN']}"}, json={"feed_url": "https://example.com/diary.xml"}, ) response.raise_for_status() print(response.json())
const response = await fetch(`${process.env.API}/automatic-newsletters/validate`, { method: "POST", headers: { Authorization: `Bearer ${process.env.BANDTOOLS_API_TOKEN}`, "Content-Type": "application/json" }, body: JSON.stringify({ feed_url: "https://example.com/diary.xml" }) }); if (!response.ok) throw new Error(`HTTP ${response.status}`); console.log(await response.json());
payload, _ := json.Marshal(map[string]any{ "feed_url": "https://example.com/diary.xml", }) req, _ := http.NewRequest("POST", os.Getenv("API")+"/automatic-newsletters/validate", bytes.NewReader(payload)) req.Header.Set("Authorization", "Bearer "+os.Getenv("BANDTOOLS_API_TOKEN")) req.Header.Set("Content-Type", "application/json") response, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) fmt.Println(string(body))
require "net/http" require "json" uri = URI("#{ENV.fetch("API")}/automatic-newsletters/validate") request = Net::HTTP::Post.new(uri, { "Authorization" => "Bearer #{ENV.fetch("BANDTOOLS_API_TOKEN")}", "Content-Type" => "application/json" }) request.body = { feed_url: "https://example.com/diary.xml" }.to_json response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) } puts JSON.parse(response.body)
String body = "{\"feed_url\":\"https://example.com/diary.xml\"}"; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(System.getenv("API") + "/automatic-newsletters/validate")) .header("Authorization", "Bearer " + System.getenv("BANDTOOLS_API_TOKEN")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse<String> response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body());
$payload = json_encode(["feed_url" => "https://example.com/diary.xml"]); $ch = curl_init(getenv("API") . "/automatic-newsletters/validate"); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer " . getenv("BANDTOOLS_API_TOKEN"), "Content-Type: application/json", ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); curl_close($ch); echo $response;
15.1 List subscribers
curl -H "Authorization: Bearer $BANDTOOLS_API_TOKEN" \ "$API/subscribers?page=1&per_page=25&sort=subscribed_recent&filter=confirmed"
import os, requests response = requests.get( f"{os.environ['API']}/subscribers", headers={"Authorization": f"Bearer {os.environ['BANDTOOLS_API_TOKEN']}"}, params={"page": 1, "per_page": 25, "sort": "subscribed_recent", "filter": "confirmed"}, ) response.raise_for_status() print(response.json())
const url = new URL(`${process.env.API}/subscribers`); url.search = new URLSearchParams({ page: "1", per_page: "25", sort: "subscribed_recent", filter: "confirmed" }); const response = await fetch(url, { headers: { Authorization: `Bearer ${process.env.BANDTOOLS_API_TOKEN}` } }); if (!response.ok) throw new Error(`HTTP ${response.status}`); console.log(await response.json());
u, _ := url.Parse(os.Getenv("API") + "/subscribers") query := u.Query() query.Set("page", "1") query.Set("per_page", "25") query.Set("sort", "subscribed_recent") query.Set("filter", "confirmed") u.RawQuery = query.Encode() req, _ := http.NewRequest("GET", u.String(), nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("BANDTOOLS_API_TOKEN")) response, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) fmt.Println(string(body))
require "net/http" require "json" uri = URI("#{ENV.fetch("API")}/subscribers") uri.query = URI.encode_www_form(page: 1, per_page: 25, sort: "subscribed_recent", filter: "confirmed") request = Net::HTTP::Get.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BANDTOOLS_API_TOKEN")}" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) } puts JSON.parse(response.body)
String query = "page=1&per_page=25&sort=subscribed_recent&filter=confirmed"; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(System.getenv("API") + "/subscribers?" + query)) .header("Authorization", "Bearer " + System.getenv("BANDTOOLS_API_TOKEN")) .GET() .build(); HttpResponse<String> response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body());
$query = http_build_query([ "page" => 1, "per_page" => 25, "sort" => "subscribed_recent", "filter" => "confirmed", ]); $ch = curl_init(getenv("API") . "/subscribers?" . $query); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer " . getenv("BANDTOOLS_API_TOKEN"), ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); curl_close($ch); echo $response;
15.2 Add a subscriber
curl -X POST "$API/subscribers" \ -H "Authorization: Bearer $BANDTOOLS_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"email_address":"fan@example.com"}'
import os, requests response = requests.post( f"{os.environ['API']}/subscribers", headers={"Authorization": f"Bearer {os.environ['BANDTOOLS_API_TOKEN']}"}, json={"email_address": "fan@example.com"}, ) response.raise_for_status() print(response.json())
const response = await fetch(`${process.env.API}/subscribers`, { method: "POST", headers: { Authorization: `Bearer ${process.env.BANDTOOLS_API_TOKEN}`, "Content-Type": "application/json" }, body: JSON.stringify({ email_address: "fan@example.com" }) }); if (!response.ok) throw new Error(`HTTP ${response.status}`); console.log(await response.json());
payload, _ := json.Marshal(map[string]any{ "email_address": "fan@example.com", }) req, _ := http.NewRequest("POST", os.Getenv("API")+"/subscribers", bytes.NewReader(payload)) req.Header.Set("Authorization", "Bearer "+os.Getenv("BANDTOOLS_API_TOKEN")) req.Header.Set("Content-Type", "application/json") response, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) fmt.Println(string(body))
require "net/http" require "json" uri = URI("#{ENV.fetch("API")}/subscribers") request = Net::HTTP::Post.new(uri, { "Authorization" => "Bearer #{ENV.fetch("BANDTOOLS_API_TOKEN")}", "Content-Type" => "application/json" }) request.body = { email_address: "fan@example.com" }.to_json response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) } puts JSON.parse(response.body)
String body = "{\"email_address\":\"fan@example.com\"}"; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(System.getenv("API") + "/subscribers")) .header("Authorization", "Bearer " + System.getenv("BANDTOOLS_API_TOKEN")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse<String> response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body());
$payload = json_encode(["email_address" => "fan@example.com"]); $ch = curl_init(getenv("API") . "/subscribers"); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer " . getenv("BANDTOOLS_API_TOKEN"), "Content-Type: application/json", ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); curl_close($ch); echo $response;
15.3 Get a subscriber
curl -H "Authorization: Bearer $BANDTOOLS_API_TOKEN" \ "$API/subscribers/$EMAIL_HASH"
import os, requests response = requests.get( f"{os.environ['API']}/subscribers/{os.environ['EMAIL_HASH']}", headers={"Authorization": f"Bearer {os.environ['BANDTOOLS_API_TOKEN']}"}, ) response.raise_for_status() print(response.json())
const response = await fetch( `${process.env.API}/subscribers/${process.env.EMAIL_HASH}`, { headers: { Authorization: `Bearer ${process.env.BANDTOOLS_API_TOKEN}` } } ); if (!response.ok) throw new Error(`HTTP ${response.status}`); console.log(await response.json());
req, _ := http.NewRequest("GET", os.Getenv("API")+"/subscribers/"+os.Getenv("EMAIL_HASH"), nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("BANDTOOLS_API_TOKEN")) response, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) fmt.Println(string(body))
require "net/http" require "json" uri = URI("#{ENV.fetch("API")}/subscribers/#{ENV.fetch("EMAIL_HASH")}") request = Net::HTTP::Get.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BANDTOOLS_API_TOKEN")}" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) } puts JSON.parse(response.body)
HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(System.getenv("API") + "/subscribers/" + System.getenv("EMAIL_HASH"))) .header("Authorization", "Bearer " + System.getenv("BANDTOOLS_API_TOKEN")) .GET() .build(); HttpResponse<String> response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body());
$ch = curl_init(getenv("API") . "/subscribers/" . getenv("EMAIL_HASH")); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer " . getenv("BANDTOOLS_API_TOKEN"), ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); curl_close($ch); echo $response;
15.4 Delete a subscriber
curl -X DELETE "$API/subscribers/$EMAIL_HASH" \ -H "Authorization: Bearer $BANDTOOLS_API_TOKEN"
import os, requests response = requests.delete( f"{os.environ['API']}/subscribers/{os.environ['EMAIL_HASH']}", headers={"Authorization": f"Bearer {os.environ['BANDTOOLS_API_TOKEN']}"}, ) response.raise_for_status()
const response = await fetch( `${process.env.API}/subscribers/${process.env.EMAIL_HASH}`, { method: "DELETE", headers: { Authorization: `Bearer ${process.env.BANDTOOLS_API_TOKEN}` } } ); if (!response.ok) throw new Error(`HTTP ${response.status}`);
req, _ := http.NewRequest("DELETE", os.Getenv("API")+"/subscribers/"+os.Getenv("EMAIL_HASH"), nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("BANDTOOLS_API_TOKEN")) response, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } response.Body.Close()
require "net/http" uri = URI("#{ENV.fetch("API")}/subscribers/#{ENV.fetch("EMAIL_HASH")}") request = Net::HTTP::Delete.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BANDTOOLS_API_TOKEN")}" Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }
HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(System.getenv("API") + "/subscribers/" + System.getenv("EMAIL_HASH"))) .header("Authorization", "Bearer " + System.getenv("BANDTOOLS_API_TOKEN")) .DELETE() .build(); HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.discarding());
$ch = curl_init(getenv("API") . "/subscribers/" . getenv("EMAIL_HASH")); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer " . getenv("BANDTOOLS_API_TOKEN"), ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_exec($ch); curl_close($ch);
15.5 Delete all subscribers
curl -X DELETE "$API/subscribers?confirm=true" \ -H "Authorization: Bearer $BANDTOOLS_API_TOKEN"
import os, requests response = requests.delete( f"{os.environ['API']}/subscribers", headers={"Authorization": f"Bearer {os.environ['BANDTOOLS_API_TOKEN']}"}, params={"confirm": "true"}, ) response.raise_for_status()
const response = await fetch(`${process.env.API}/subscribers?confirm=true`, { method: "DELETE", headers: { Authorization: `Bearer ${process.env.BANDTOOLS_API_TOKEN}` } }); if (!response.ok) throw new Error(`HTTP ${response.status}`);
req, _ := http.NewRequest("DELETE", os.Getenv("API")+"/subscribers?confirm=true", nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("BANDTOOLS_API_TOKEN")) response, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } response.Body.Close()
require "net/http" uri = URI("#{ENV.fetch("API")}/subscribers") uri.query = URI.encode_www_form(confirm: true) request = Net::HTTP::Delete.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BANDTOOLS_API_TOKEN")}" Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }
HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(System.getenv("API") + "/subscribers?confirm=true")) .header("Authorization", "Bearer " + System.getenv("BANDTOOLS_API_TOKEN")) .DELETE() .build(); HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.discarding());
$ch = curl_init(getenv("API") . "/subscribers?confirm=true"); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer " . getenv("BANDTOOLS_API_TOKEN"), ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_exec($ch); curl_close($ch);
15.6 Import subscribers
Multipart CSV upload
A header row is auto-detected, and the first column-with-an-email per row wins.
Only text/csv (and the common aliases application/csv, application/vnd.ms-excel, text/plain) with a .csv extension are accepted.
The hard upload cap is 20 MiB.
Submitting a non-CSV file (e.g. a PDF) returns 415 unsupported_media_type.
curl -i -X POST "$API/subscribers/imports" \ -H "Authorization: Bearer $BANDTOOLS_API_TOKEN" \ -F "file=@subscribers.csv;type=text/csv"
import os, requests with open("subscribers.csv", "rb") as f: response = requests.post( f"{os.environ['API']}/subscribers/imports", headers={"Authorization": f"Bearer {os.environ['BANDTOOLS_API_TOKEN']}"}, files={"file": ("subscribers.csv", f, "text/csv")}, ) response.raise_for_status() print(response.json())
import { readFileSync } from "node:fs"; const body = new FormData(); body.append("file", new Blob([readFileSync("subscribers.csv")], { type: "text/csv" }), "subscribers.csv"); const response = await fetch(`${process.env.API}/subscribers/imports`, { method: "POST", headers: { Authorization: `Bearer ${process.env.BANDTOOLS_API_TOKEN}` }, body }); if (!response.ok) throw new Error(`HTTP ${response.status}`); console.log(await response.json());
var body bytes.Buffer writer := multipart.NewWriter(&body) part, _ := writer.CreatePart(textproto.MIMEHeader{ "Content-Disposition": []string{`form-data; name="file"; filename="subscribers.csv"`}, "Content-Type": []string{"text/csv"}, }) file, _ := os.Open("subscribers.csv") defer file.Close() io.Copy(part, file) writer.Close() req, _ := http.NewRequest("POST", os.Getenv("API")+"/subscribers/imports", &body) req.Header.Set("Authorization", "Bearer "+os.Getenv("BANDTOOLS_API_TOKEN")) req.Header.Set("Content-Type", writer.FormDataContentType()) response, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer response.Body.Close() data, _ := io.ReadAll(response.Body) fmt.Println(string(data))
require "net/http" require "json" uri = URI("#{ENV.fetch("API")}/subscribers/imports") request = Net::HTTP::Post.new(uri, { "Authorization" => "Bearer #{ENV.fetch("BANDTOOLS_API_TOKEN")}" }) File.open("subscribers.csv") do |file| request.set_form([ [ "file", file, { filename: "subscribers.csv", content_type: "text/csv" } ] ], "multipart/form-data") end response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) } puts JSON.parse(response.body)
String boundary = "----BandToolsBoundary" + System.currentTimeMillis(); byte[] fileBytes = Files.readAllBytes(Path.of("subscribers.csv")); ByteArrayOutputStream buffer = new ByteArrayOutputStream(); buffer.writeBytes(("--" + boundary + "\r\n").getBytes()); buffer.writeBytes("Content-Disposition: form-data; name=\"file\"; filename=\"subscribers.csv\"\r\n".getBytes()); buffer.writeBytes("Content-Type: text/csv\r\n\r\n".getBytes()); buffer.writeBytes(fileBytes); buffer.writeBytes(("\r\n--" + boundary + "--\r\n").getBytes()); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(System.getenv("API") + "/subscribers/imports")) .header("Authorization", "Bearer " + System.getenv("BANDTOOLS_API_TOKEN")) .header("Content-Type", "multipart/form-data; boundary=" + boundary) .POST(HttpRequest.BodyPublishers.ofByteArray(buffer.toByteArray())) .build(); HttpResponse<String> response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body());
$ch = curl_init(getenv("API") . "/subscribers/imports"); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, [ "file" => new CURLFile("subscribers.csv", "text/csv", "subscribers.csv"), ]); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer " . getenv("BANDTOOLS_API_TOKEN"), ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); curl_close($ch); echo $response;
Inline JSON email list
Convenient when the addresses are already in memory.
email_addresses must contain between 1 and 10,000 entries.
Each individual address is capped at 256 bytes; an oversize entry returns 413 payload_too_large.
curl -i -X POST "$API/subscribers/imports" \ -H "Authorization: Bearer $BANDTOOLS_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "email_addresses": [ "fan-one@example.com", "fan-two@example.com", "fan-three@example.com" ] }'
import os, requests response = requests.post( f"{os.environ['API']}/subscribers/imports", headers={"Authorization": f"Bearer {os.environ['BANDTOOLS_API_TOKEN']}"}, json={"email_addresses": [ "fan-one@example.com", "fan-two@example.com", "fan-three@example.com", ]}, ) response.raise_for_status() import_id = response.json()["data"]["id"]
const response = await fetch(`${process.env.API}/subscribers/imports`, { method: "POST", headers: { Authorization: `Bearer ${process.env.BANDTOOLS_API_TOKEN}`, "Content-Type": "application/json" }, body: JSON.stringify({ email_addresses: [ "fan-one@example.com", "fan-two@example.com", "fan-three@example.com" ] }) }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const { data } = await response.json(); console.log(data.id);
payload, _ := json.Marshal(map[string]any{ "email_addresses": []string{ "fan-one@example.com", "fan-two@example.com", "fan-three@example.com", }, }) req, _ := http.NewRequest("POST", os.Getenv("API")+"/subscribers/imports", bytes.NewReader(payload)) req.Header.Set("Authorization", "Bearer "+os.Getenv("BANDTOOLS_API_TOKEN")) req.Header.Set("Content-Type", "application/json") response, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) fmt.Println(string(body))
require "net/http" require "json" uri = URI("#{ENV.fetch("API")}/subscribers/imports") request = Net::HTTP::Post.new(uri, { "Authorization" => "Bearer #{ENV.fetch("BANDTOOLS_API_TOKEN")}", "Content-Type" => "application/json" }) request.body = { email_addresses: %w[fan-one@example.com fan-two@example.com fan-three@example.com] }.to_json response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) } puts JSON.parse(response.body).dig("data", "id")
String body = """ { "email_addresses": [ "fan-one@example.com", "fan-two@example.com", "fan-three@example.com" ] } """; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(System.getenv("API") + "/subscribers/imports")) .header("Authorization", "Bearer " + System.getenv("BANDTOOLS_API_TOKEN")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse<String> response = HttpClient.newHttpClient() .send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body());
$payload = json_encode([ "email_addresses" => [ "fan-one@example.com", "fan-two@example.com", "fan-three@example.com", ], ]); $ch = curl_init(getenv("API") . "/subscribers/imports"); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer " . getenv("BANDTOOLS_API_TOKEN"), "Content-Type: application/json", ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); curl_close($ch); echo $response;
A successful response returns 202 Accepted:
{ "data": { "id": "imp_abc123", "status": "processing", "imported_count": null, "skipped_count": null, "failed_email_addresses": [], "filename": "subscribers.csv", "error": null, "created_at": "2026-04-22T12:00:00Z", "completed_at": null }, "meta": { "request_id": "..." } }
Poll every 2–5 seconds until status is completed or error:
curl -H "Authorization: Bearer $BANDTOOLS_API_TOKEN" \ "$API/subscribers/imports/imp_abc123"
import os, time, requests while True: response = requests.get( f"{os.environ['API']}/subscribers/imports/{import_id}", headers={"Authorization": f"Bearer {os.environ['BANDTOOLS_API_TOKEN']}"}, ) response.raise_for_status() status = response.json()["data"]["status"] if status in {"completed", "error"}: print(response.json()) break time.sleep(3)
while (true) { const response = await fetch( `${process.env.API}/subscribers/imports/${importId}`, { headers: { Authorization: `Bearer ${process.env.BANDTOOLS_API_TOKEN}` } } ); if (!response.ok) throw new Error(`HTTP ${response.status}`); const { data } = await response.json(); if (data.status === "completed" || data.status === "error") { console.log(data); break; } await new Promise(r => setTimeout(r, 3000)); }
for { req, _ := http.NewRequest("GET", os.Getenv("API")+"/subscribers/imports/"+importID, nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("BANDTOOLS_API_TOKEN")) response, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } var payload struct { Data struct { Status string `json:"status"` } `json:"data"` } json.NewDecoder(response.Body).Decode(&payload) response.Body.Close() if payload.Data.Status == "completed" || payload.Data.Status == "error" { fmt.Println(payload.Data.Status) break } time.Sleep(3 * time.Second) }
require "net/http" require "json" loop do uri = URI("#{ENV.fetch("API")}/subscribers/imports/#{import_id}") request = Net::HTTP::Get.new(uri) request["Authorization"] = "Bearer #{ENV.fetch("BANDTOOLS_API_TOKEN")}" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) } data = JSON.parse(response.body).fetch("data") break pp data if %w[completed error].include?(data["status"]) sleep 3 end
HttpClient client = HttpClient.newHttpClient(); while (true) { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(System.getenv("API") + "/subscribers/imports/" + importId)) .header("Authorization", "Bearer " + System.getenv("BANDTOOLS_API_TOKEN")) .GET() .build(); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.body().contains("\"status\":\"completed\"") || response.body().contains("\"status\":\"error\"")) { System.out.println(response.body()); break; } Thread.sleep(3000); }
while (true) { $ch = curl_init(getenv("API") . "/subscribers/imports/" . $importId); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer " . getenv("BANDTOOLS_API_TOKEN"), ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); curl_close($ch); $data = json_decode($response, true)["data"]; if (in_array($data["status"], ["completed", "error"], true)) { print_r($data); break; } sleep(3); }
Behaviour
- Only one import runs at a time per account
- Duplicates and reserved addresses count as skipped; unconfirmed subscribers remain unconfirmed
- The import stops early if the account reaches its subscriber limit mid-import. Already-imported rows are kept and the final
imported_countreflects what actually landed - Polling a completed import is idempotent