Preview
Preview
Preview a campaign's emails against 5 synthetic leads that match its Ideal Customer Profile (ICP), using the same generation engine as a real run. This is the pre-flight step in the generation workflow: you set up your lead structure and options (the same ones you'd use for a batch run), preview the output to validate tone, structure, and personalization, then process the full CSV with Generate Batch.
Requires the campaigns:write scope.
Billing: Every call costs 5 credits. If some synthetic leads fail to generate, you are refunded for the failed leads; if all fail, you are fully refunded. The credit cost is fixed at 5 per call (there is no free first preview on the API).
Readiness: The campaign's email instructions must be built first (after saving campaign strategy). If they aren't ready yet, the request returns
409 NOT_READY— wait about a minute and retry. No credits are charged when this happens.
Generate a preview
POST /api/v1/campaigns/:id/preview · scope campaigns:write
Path parameters
| Parameter | Type | Description |
|---|---|---|
id | string (UUID) | Campaign ID. |
Headers
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer <API_KEY> (or X-API-Key). |
Content-Type | Yes | application/json. |
Idempotency-Key | No | A unique key to safely retry. A replayed key returns the original result without re-charging. A failed run releases the key so you can retry. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
sample.headers | string[] | Yes | Your lead CSV column headers (≥ 1). The synthetic leads mirror this structure. |
sample.rows | string[][] | No | A few sample data rows to anchor realism. |
sample.field_mapping | object | string | No | Maps logical fields (e.g. email, firstName, companyName) to your header names. |
options.email_count | number | string | Yes | Emails per lead. Integer 1–4. |
options.email_signature | string | Yes* | Sender signature, inline. Required unless signature_id is provided. Multi-line: use \n between lines. See Signature format. |
options.signature_id | string (UUID) | Yes* | ID of a saved signature (from GET /api/v1/signatures) to use instead of email_signature. Provide exactly one of the two. |
options.additional_details | string | No | Free-text campaign context passed to the writer ("Added Campaign Details"). |
options.approved_ctas | string | string[] | No | Approved calls-to-action. Defaults to Question CTA, Lead Magnet CTA, Direct Meeting CTA. |
options.enrichment_enabled | boolean | No | Preview enriched (true) vs. non-enriched (false, default) output. See note below. |
options.enrichment_mapping | object | No | Accepted for parity with a real run, but not required for preview. |
options.assistant_id | string | No | Override the assistant used for this preview (advanced; defaults to the campaign's). |
options.vector_id | string | No | Override the knowledge-base vector store used for this preview (advanced; defaults to the campaign's). |
* Exactly one of email_signature or signature_id is required (providing both is a 400). Preview uses the same required config as a real run so the output faithfully reflects what generation will produce (including the real signature). campaign_angle is a deprecated alias of additional_details.
Enrichment in preview. Set
enrichment_enabledtotrueorfalseto preview the campaign with or without enriched output. Preview uses synthetic leads and simulates the enrichment columns, so no per-lead data orenrichment_mappingis required, and the cost stays a flat 5 credits.
Example request
curl -X POST "https://app.machfive.io/api/v1/campaigns/CAMPAIGN_ID/preview" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1b8e2a-..." \
-d '{
"sample": {
"headers": ["First Name", "Company Name", "Title", "Email"],
"rows": [["Jane", "Acme Inc", "VP Marketing", "jane@acme.com"]],
"field_mapping": { "email": "Email", "firstName": "First Name", "companyName": "Company Name" }
},
"options": {
"email_count": 2,
"email_signature": "Sam Rivera\nMachFive | Founder\n500 Market St, San Francisco, CA 94105"
}
}'Response (200 OK)
{
"preview_id": "uuid",
"status": "success",
"credits_charged": 5,
"success_count": 5,
"fail_count": 0,
"headers": ["#", "Subject Line 1", "Email 1", "Subject Line 2", "Email 2", "First Name", "Company Name", "Title"],
"rows": [["1", "...", "...", "...", "...", "Jane", "Acme Inc", "VP Marketing"]],
"synthetic_leads": [ { "First Name": "Jane", "email_address": "jane@acme.com" } ],
"generated_emails": [
{
"lead_index": 0,
"lead_data": { "First Name": "Jane" },
"emails": [ { "subject": "...", "body": "..." } ],
"status": "success"
}
]
}| Field | Type | Description |
|---|---|---|
preview_id | string (UUID) | Stored preview ID. |
status | string | success (all leads) or partial (some leads failed). |
credits_charged | number | Net credits charged after any partial-failure refunds. |
success_count / fail_count | number | Per-lead generation outcome counts. |
headers / rows | array | CSV-shaped table data for display. |
synthetic_leads | array | The generated synthetic leads. |
generated_emails | array | Per-lead generated emails (and any per-lead errors). |
Error responses
| Status | Error | Description |
|---|---|---|
| 400 | BAD_REQUEST | Invalid body (e.g. missing sample.headers); email_count missing or not 1–4; or no signature (neither email_signature nor signature_id) or both provided. |
| 401 | UNAUTHORIZED | Missing or invalid API key. |
| 402 | INSUFFICIENT_CREDITS | Not enough balance or PAYG headroom for 5 credits. detail includes required, available, payg_enabled, pct_of_cap. |
| 403 | FORBIDDEN | Key missing the campaigns:write scope. |
| 404 | NOT_FOUND | Campaign not found or not in this workspace, or signature_id not found in this workspace. |
| 409 | CONFLICT | An Idempotency-Key was reused with a different body, or a request with the same key is still in flight. |
| 409 | NOT_READY | Campaign email instructions aren't built yet; retry shortly. No credits charged. |
| 429 | RATE_LIMITED | Per-key rate limit exceeded; honor Retry-After. (This write fails closed when the limiter is unavailable.) |
| 500 | INTERNAL_ERROR | Generation failed. Any charged credits are refunded. |