Recipes

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.

StepRouteKey scope
Preview the importPOST /contacts/import/previewWRITE_CONTACTS
Commit the importPOST /contacts/import/commitBULK_CONTACTS
Create the campaignPOST /campaignsWRITE_CAMPAIGNS
Set sendersPUT /campaigns/:id/sendersWRITE_CAMPAIGNS
Enrol leadsPATCH /contactsBULK_CONTACTS
PreflightPOST /campaigns/:id/preflightSEND_CAMPAIGNS
Today's planGET /campaigns/:id/send-planREAD_CAMPAIGNS
StartPOST /campaigns/:id/startSEND_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"]
  }'
OptionWhat to know
mappingExactly one column maps to email. Other targets: first_name, last_name, company, phone, subscribed, categories, verification_status, consent, or custom with custom_key
verification_statusKeeps another service's verdicts; verification_provider names its vocabulary, or omit it to have values recognised one by one
consentA date records when consent was given, yes records it at import time, no or withdrawn clears it
dedupskip, update or create_duplicate for addresses you already hold
category_idsLabels on every imported contact; useful to select this list in step 4
campaign_idsEnrol 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_after days after the contact's previous step. The first step's wait_after defaults to 0, follow-ups to 3. 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_reply to override.
  • daily_limit is 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 rateWhat happens
4% or moreRefused 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 leadsStarts

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_ID

campaign 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.

On this page