Recipe: launch a campaign from a CSV
Import a lead list, create a campaign with its steps and senders, run preflight and start it over the AY Inbox API or CLI, with the launch list check explained.
Launching a campaign from a CSV takes five calls. Preview the import, commit it with a column mapping, create the campaign with its steps, run preflight, then start it. The start refuses a list projected at 4% bounce or more unless you pass acknowledge_list_risk, and nothing sends until the start succeeds.
A campaign in AY Inbox is a sender pool, a schedule and an ordered list of steps sent to a set of leads (contacts enrolled in it). The campaign's daily_limit caps each mailbox at 50 of its emails a day by default, and a mailbox waits at least 600 seconds between sends by default. The product side is in the campaigns guide; the routes are in the contacts and campaigns references.
| Step | Route | Key scope |
|---|---|---|
| Preview the import | POST /contacts/import/preview | WRITE_CONTACTS |
| Commit the import | POST /contacts/import/commit | BULK_CONTACTS |
| Create the campaign | POST /campaigns | WRITE_CAMPAIGNS |
| Set senders | PUT /campaigns/:id/senders | WRITE_CAMPAIGNS |
| Enrol leads | PATCH /contacts | BULK_CONTACTS |
| Preflight | POST /campaigns/:id/preflight | SEND_CAMPAIGNS |
| Today's plan | GET /campaigns/:id/send-plan | READ_CAMPAIGNS |
| Start | POST /campaigns/:id/start | SEND_CAMPAIGNS |
1. Preview the import
Upload the file as multipart/form-data with a file field. CSV and XLSX are accepted, up to 50 MB. Nothing is written.
curl -s -X POST "https://api.ayinbox.com/v1/contacts/import/preview" \
-H "Authorization: Bearer aybox_..." \
-F "[email protected]"The answer has columns, has_header, total_rows, up to 20 sample_rows and a suggested_mapping: one {index, target} per column. A header that names an existing custom field is suggested as custom with that field's spelling, a column of addresses is suggested as email whatever its header when no header named one, and anything unmatched is ignore. Treat the suggestion as a default and check it.
2. Commit with a mapping
Send the same file again with an options form field holding JSON.
curl -s -X POST "https://api.ayinbox.com/v1/contacts/import/commit" \
-H "Authorization: Bearer aybox_..." \
-H "Idempotency-Key: import-q3-leads" \
-F "[email protected]" \
-F 'options={
"mapping": [
{"index": 0, "target": "email"},
{"index": 1, "target": "first_name"},
{"index": 2, "target": "company"},
{"index": 3, "target": "custom", "custom_key": "industry"},
{"index": 4, "target": "verification_status", "verification_provider": "zerobounce"},
{"index": 5, "target": "consent"}
],
"dedup": "update",
"has_header": true,
"category_ids": ["LABEL_ID"]
}'| Option | What to know |
|---|---|
mapping | Exactly one column maps to email. Other targets: first_name, last_name, company, phone, subscribed, categories, verification_status, consent, or custom with custom_key |
verification_status | Keeps another service's verdicts; verification_provider names its vocabulary, or omit it to have values recognised one by one |
consent | A date records when consent was given, yes records it at import time, no or withdrawn clears it |
dedup | skip, update or create_duplicate for addresses you already hold |
category_ids | Labels on every imported contact; useful to select this list in step 4 |
campaign_ids | Enrol the contacts straight into existing campaigns |
The answer counts imported, updated, skipped and failed (they sum to total), lists per-row errors, and carries a quality block: malformed, disposable and role addresses. quality.flagged is advisory; the list check that can stop a launch comes at start. Files are capped at 50,000 rows. For a large file, use background imports (POST /contacts/imports, then /analyze and /start), which take the same mapping and survive a dropped connection. Keeping your own list and verification is covered in bring your own leads and mailboxes.
3. Create the campaign with steps
curl -s -X POST "https://api.ayinbox.com/v1/campaigns" \
-H "Authorization: Bearer aybox_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: campaign-q3" \
-d '{
"name": "Q3 outbound",
"stop_on_reply": true,
"unsubscribe_header": true,
"daily_limit": 30,
"timezone": "America/New_York",
"steps": [
{"name": "Intro", "subject": "Quick question, {{.FirstName}}", "body_plain": "Hi {{.FirstName}}, ..."},
{"name": "Bump", "body_plain": "Just bumping this, {{.FirstName}}.", "wait_after": 3},
{"name": "Last", "body_plain": "Closing the loop.", "wait_after": 4}
]
}'- Steps are connected in order. Each step waits its own
wait_afterdays after the contact's previous step. The first step'swait_afterdefaults to0, follow-ups to3. A step with no flow connection after it ends the sequence. - Threading follows the subject. A step with no subject, or the same subject as the conversation, is sent as a reply in the same thread. A step with a subject of its own opens a new conversation. Set
thread_replyto override. daily_limitis per mailbox, from 3 to 5,000. Keep it at or below the default 50 unless a mailbox has the history to justify more.- Merge fields use
{{.FirstName}},{{.Company}}and custom fields such as{{.industry}}. See personalization.
The campaign is created as a draft. Nothing sends yet.
4. Add senders and leads
Senders. With the default sender_strategy: "tags", the pool comes from email_tag_ids, and with no tag and no explicit sender it falls back to every active mailbox in the workspace. To name mailboxes, set sender_strategy: "explicit" on the campaign and replace the pool:
curl -s -X PUT "https://api.ayinbox.com/v1/campaigns/CAMPAIGN_ID/senders" \
-H "Authorization: Bearer aybox_..." \
-H "Content-Type: application/json" \
-d '{"senders": [{"email_account_id": "MAILBOX_1", "weight": 1}, {"email_account_id": "MAILBOX_2", "weight": 1}]}'An explicit pool never widens to the whole workspace. If it empties, the campaign parks at paused_no_accounts.
Leads. Select the imported list by its label and enrol it:
{ "all": true, "filters": { "category_ids": ["LABEL_ID"] }, "add_campaigns": ["CAMPAIGN_ID"] }Send that to PATCH /contacts. If you created the campaign before the import, campaign_ids in the commit options does the same in one step.
5. Preflight and plan
POST /campaigns/:id/preflight runs the campaign's checks and sends no mail. It returns passed, a score and a list of checks, each with key, passed, severity, message and remediation: tracking domain, unsubscribe header, daily limit, schedule window, A/B configuration, content score and list_quality, the same bounce projection the start applies. Fix what it reports before starting.
GET /campaigns/:id/send-plan answers what the campaign sends today and every limit that decided it: projected_today, a bottleneck, the limits that removed volume (warmup graduation, the campaign limit, other campaigns on the same mailboxes, sending windows) and each mailbox's cap_today. On a draft it shows the not_running limit, so read it again after the start for real numbers, and before raising any cap.
6. Start
curl -s -X POST "https://api.ayinbox.com/v1/campaigns/CAMPAIGN_ID/start" \
-H "Authorization: Bearer aybox_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: start-q3" \
-d '{}'Starting puts real mail on the wire, so it needs SEND_CAMPAIGNS (for a member, Send campaigns). It answers {"status": "started", "waiting_for_leads": false}.
The start first measures the list. On a list of at least 50 deliverable leads, counting only addresses verification actually rejected:
| Projected bounce rate | What happens |
|---|---|
| 4% or more | Refused with list_bounce_risk and the number |
| 2% to under 4% | Starts; preflight's list_quality check flags it |
| Under 2%, or fewer than 50 deliverable leads | Starts |
For a list you verified with another service, repeat the request with {"acknowledge_list_risk": true}. Other refusals: leads_undeliverable when every remaining lead was refused by verification (the campaign parks at paused_undeliverable), and empty_step_body when an email step has no body. An Idempotency-Key makes a retried start replay the first answer instead of running twice.
The same in the CLI
The import routes take a multipart file, which the ayinbox CLI does not build, so upload the CSV with curl as above, or create contacts from JSON with ayinbox contact create --input @contacts.json. The rest maps to commands:
ayinbox campaign create --input @campaign.json --idempotency-key campaign-q3
ayinbox campaign set-senders CAMPAIGN_ID --input @senders.json
ayinbox api /contacts -X PATCH --input @enrol.json
ayinbox campaign preflight CAMPAIGN_ID
ayinbox campaign plan CAMPAIGN_ID
ayinbox campaign start CAMPAIGN_IDcampaign start prompts before it sends, and with no terminal it refuses. Pass --yes only when a person asked for that specific launch, never globally to silence prompts. Every command and flag is on the CLI page.
Frequently asked questions
Can I create and start a cold email campaign entirely over the API?
Yes. Import contacts with POST /contacts/import/commit, create the campaign and its steps with POST /campaigns, run POST /campaigns/:id/preflight, then POST /campaigns/:id/start. Starting needs the SEND_CAMPAIGNS scope.
Why was my campaign start refused with list_bounce_risk?
On a list of at least 50 deliverable leads, AY Inbox refuses a launch whose projected bounce rate is 4% or more, counting only addresses verification rejected. Clean the list, or repeat the start with acknowledge_list_risk: true for a list you verified elsewhere.
How big can a CSV import be?
Up to 50 MB and 50,000 rows per file. Small files can be committed inside the request; large ones go through background imports, which run on the server in chunks.
Which import field keeps a verification verdict from another service?
verification_status. Map the other service's result column to it in the import mapping; the verdicts are stored as imported, and the background check leaves those contacts alone.
Can the ayinbox CLI upload the CSV file?
The import routes take a multipart file upload, which the CLI does not build, so upload the file with curl or your HTTP client. The CLI covers the rest: creating contacts from JSON, the campaign, preflight and start.
Account and organization endpoints
Authentication, sessions, your profile and security settings, organization governance, teams, and subscription billing.
Recipe: get notified of positive replies
Receive a signed webhook when a prospect replies to a campaign, read its intent and confidence, and route interested replies to Slack, a CRM or your code.