Package and file formats
Column names on this page show the shape of each file. Your onboarding contact confirms the exact template for your account, and the upload preview shows how each column was read.
Files you send
Section titled “Files you send”People to match
Section titled “People to match”CSV, UTF-8, with a header row. Send whichever identifiers you hold. Each agreeing identifier counts toward strictness.
email,phone,first_name,last_name,address,city,state,zipdana.reyes@example.com,(555) 010-4477,Dana,Reyes,12 Elm St,Springfield,IL,62704lee.m@example.org,,Lee,Morgan,,,,Results for an Outcome
Section titled “Results for an Outcome”One row per person who did it, with the time it happened. A result is attributed by when it happened, not when the file arrived.
email,phone,event_time,outcomedana.reyes@example.com,,2026-10-14T15:42:07Z,Book a consultationevent_time is ISO 8601 in UTC. Only confident matches count. See Results returning.
Suppression list
Section titled “Suppression list”The same identifier columns as a people file. Everyone matched is kept out of every list built after the upload.
Files you receive
Section titled “Files you receive”A campaign package
Section titled “A campaign package”A ZIP built on Moonbrush servers. See Delivery for the folder layout.
An illustrative manifest.json:
{ "campaign": "Spring consultations", "built_at": "2026-10-14T09:00:00Z", "destination_layout": "meta", "overlap_policy": "ship_once_highest_priority", "audiences": [ { "name": "Lapsed members", "priority": 1, "members": 48210, "withheld": { "suppressed": 1312, "held_back": 0 }, "groups": [ { "name": "Researches first", "members": 17420, "sharpness": "Clear", "score_version": "2026.10" }, { "name": "Deal seekers", "members": 12980, "sharpness": "Sharp", "score_version": "2026.10" } ] } ]}Withheld people appear only as counts. No identifier for a withheld person is in the package.
Member file
Section titled “Member file”One row per hashed identifier. A person with two emails has two rows.
row_key,email_sha256,phone_sha256,fn_sha256,ln_sha256,zip,countryr_5c1e9a,3c8d1f…e07b,9a41c2…5d10,7f2a0e…c3b9,d06b4c…81aa,62704,USHash values are truncated here for width. Real values are 64 hexadecimal characters.
Hashing reference
Section titled “Hashing reference”SHA-256, lowercase hexadecimal, unsalted. Normalize first, then hash. Never hash an empty value, and never hash ZIP.
import hashlibimport re
def sha256(value: str) -> str: return hashlib.sha256(value.encode("utf-8")).hexdigest()
def hash_email(email: str) -> str | None: e = email.strip().lower() return sha256(e) if e else None
def hash_phone_e164(phone: str, default_country_code: str = "1", keep_plus: bool = True) -> str | None: digits = re.sub(r"\D", "", phone) if not digits: return None if len(digits) == 10: # national number without country code digits = default_country_code + digits e164 = ("+" if keep_plus else "") + digits return sha256(e164)
def hash_name(name: str) -> str | None: n = name.strip().lower() return sha256(n) if n else None
print(hash_email(" Dana.Reyes@Example.com "))import { createHash } from "node:crypto";
const sha256 = (v) => createHash("sha256").update(v, "utf8").digest("hex");
export const hashEmail = (email) => { const e = email.trim().toLowerCase(); return e ? sha256(e) : null;};
export const hashPhoneE164 = (phone, { countryCode = "1", keepPlus = true } = {}) => { let digits = phone.replace(/\D/g, ""); if (!digits) return null; if (digits.length === 10) digits = countryCode + digits; // national number return sha256((keepPlus ? "+" : "") + digits);};
export const hashName = (name) => { const n = name.trim().toLowerCase(); return n ? sha256(n) : null;};