Getting started
Everything the API needs is a base URL, your key, and JSON.
| Base URL | https://app.contactformblaster.com/api/v1 |
| Format | JSON request bodies, JSON responses (Content-Type: application/json) |
| Authentication | Bearer token — your API key |
| Rate limit | 60 requests per minute, per key |
| Max body size | 50 MB (roughly 30,000 campaign rows) |
Authentication
Send your API key as a Bearer token on every request. Generate one on the API Access page in your dashboard, or with POST /apikeys/generate.
Authorization: Bearer sk_live_your_api_key_here
Your key is shown only once. We store it hashed, so it can never be displayed again after it is created. Store it somewhere safe — if it is lost, generate a new one (which immediately invalidates the old one).
A request with a missing, malformed, or revoked key returns 401:
{
"success": false,
"message": "Unauthorized",
"error_code": "UNAUTHORIZED"
}
Response format
Every response — including errors and 404s — uses the same envelope, so you can branch on success before anything else.
Success
{
"success": true,
"message": "",
"data": { }
}
Error
{
"success": false,
"message": "Readable message",
"error_code": "ERROR_CODE"
}
Branch your error handling on error_code (stable) rather than message (human-readable, may be reworded). Some errors add a data object with specifics — for example every field that failed validation. See Error codes.
Rate limiting
Each API key may make 60 requests per minute. The window is fixed: it resets on the minute rather than sliding. Every authenticated response carries your current standing:
X-RateLimit-Limit: 60 X-RateLimit-Remaining: 45 X-RateLimit-Reset: 1745413200
X-RateLimit-Reset is a Unix timestamp for when the window resets. Exceeding the limit returns 429 with error_code: "RATE_LIMIT_EXCEEDED" and a Retry-After header (in seconds).
Separately, repeated failed authentication attempts from one IP address are throttled: after 20 failures in a minute, further invalid requests from that address get 429 with TOO_MANY_AUTH_FAILURES until the minute is up. Requests carrying a valid key are never affected, so a correctly configured client never sees this.
Pagination
List endpoints accept page (default 1) and limit (default 50, maximum 100 — higher values are clamped, not rejected). They return a pagination object with page, limit, and total (total records, not pages).
Campaign lifecycle
Four calls take a campaign from raw data to running.
- Create — upload your rows. The campaign is saved as a
draft; no credits are charged. - Fields — set the sender details and the message. The campaign becomes
configured. - Preview — optional. Render the message for one row exactly as it will be sent.
- Submit — charge credits and start processing. The campaign becomes
processing(orqueued).
Processing is asynchronous. Poll campaign detail for progress, or supply a webhook at submit time to be notified when it finishes.
Statuses
| Status | Meaning |
|---|---|
draft | Created, not yet configured. Deletable. |
configured | Fields set, ready to submit. Deletable. |
queued | Submitted and waiting for a processing slot. |
processing | Actively submitting to websites. Can be paused. |
paused | Stopped part-way. Can be resumed; credits are not refunded on pause. |
completed | Every row has been processed. |
failed | Stopped by an unrecoverable error. |
Credit cost
Credits are charged once, at submit time: 2 credits per website, plus 1 more per website when AI personalization will actually run. Rows that turn out to be unreachable are partially or fully refunded automatically as they are processed.
Quick start
A complete campaign, end to end, with curl.
1 — Create the campaign
curl -X POST https://app.contactformblaster.com/api/v1/campaigns/create \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Q3 Outreach",
"rows": [
{ "site_url": "https://example-one.com", "business_name": "Example One" },
{ "site_url": "https://example-two.com", "business_name": "Example Two" }
]
}'
2 — Configure campaign fields
curl -X POST https://app.contactformblaster.com/api/v1/campaigns/$CAMPAIGN_ID/fields \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"fields": {
"{first_name}": "John", "{last_name}": "Doe", "{full_name}": "John Doe",
"{email}": "john@acme.com", "{phone_country_code}": "1", "{phone}": "5551234567",
"{city}": "Austin", "{state_province}": "TX", "{country}": "USA",
"{company_name}": "Acme Corp", "{misc_value}": "N/A",
"{subject}": "Question for {contact_business_name}",
"{message}": "Hi {contact_business_name}, I am {full_name} at {company_name}."
}
}'
3 — Preview one row
curl -X POST https://app.contactformblaster.com/api/v1/campaigns/$CAMPAIGN_ID/preview \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{ "sample_row_index": 0 }'
4 — Submit
curl -X POST https://app.contactformblaster.com/api/v1/campaigns/$CAMPAIGN_ID/submit \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{ "webhook_url": "https://your-app.com/hooks/campaign-done" }'
5 — Track progress
curl https://app.contactformblaster.com/api/v1/campaigns/$CAMPAIGN_ID \ -H "Authorization: Bearer $API_KEY"
API keys
One key per account, shared with the web app.
Your account has exactly one API key at a time. It is the same key shown on the API Access page — generating a key in either place immediately replaces and invalidates the other. There is no grace period and no way to run two keys at once.
Generates a new key, invalidating the existing one. The request you make with the old key still succeeds — the replacement takes effect immediately afterwards.
Body parameters
| Field | Type | Description |
|---|---|---|
name optional | string | A label for your own reference. Defaults to "Primary API Key". |
Request
{ "name": "Primary API Key" }
Response
{
"success": true,
"message": "API key generated successfully",
"data": {
"api_key": "sk_live_new123456",
"name": "Primary API Key",
"created_at": "2026-05-02T12:00:00Z",
"previous_key_invalidated": true
}
}
api_key appears in this response and nowhere else, ever again.
Metadata about your current key. Returns 404 with API_KEY_NOT_FOUND if you have none.
Response
{
"success": true,
"message": "",
"data": {
"name": "Primary API Key",
"masked_key": "sk_live_****3456",
"created_at": "2026-05-02T12:00:00Z",
"last_used_at": "2026-05-02T13:10:00Z"
}
}
Form fields
The vocabulary available to your message templates.
Lists every field you can set on a campaign. Pass a campaign id to also get that campaign's own data columns as tokens.
Query parameters
| Parameter | Type | Description |
|---|---|---|
campaign_id optional | string | Include this campaign's uploaded columns in uploaded_fields. |
Response
{
"success": true,
"message": "",
"data": {
"required_fields": [
{ "id": "{name}", "label": "Name" },
{ "id": "{email}", "label": "Email" }
],
"optional_fields": [
{ "id": "{title}", "label": "Title" }
],
"uploaded_fields": [
{ "id": "{contact_business_name}", "label": "business_name" }
],
"field_presets": [
{ "id": 10, "name": "Business Info Pack" }
]
}
}
| Key | What it contains |
|---|---|
required_fields | Fields that must have a value before a campaign can be submitted. |
optional_fields | Extra details used when a target form asks for them. |
uploaded_fields | One {contact_*} token per column of your uploaded data. Only present when campaign_id is given. |
field_presets | Your saved field presets. Use an id here as field_presets when setting fields. |
Using tokens
Any field id can be embedded in {subject} or {message} and is replaced per row when the form is submitted:
"{message}": "Hi {contact_business_name}, I'm {full_name} from {company_name}."
Tokens work in {subject} and {message} only. A token inside any other field is rejected with TOKENS_NOT_ALLOWED, and a token that does not exist for the campaign is rejected with UNKNOWN_TOKEN. If a row has no value for a token, the token is left as written rather than leaving a blank gap.
Building a campaign
Create from your own data, map the message, preview, submit.
Creates a draft campaign from structured rows. Column names are entirely yours — there are no required keys. Each object in rows is one target website plus whatever data you want available to your message.
Body parameters
| Field | Type | Description |
|---|---|---|
name required | string | Campaign name, up to 120 characters. |
rows required | array | One object per row. Maximum 30,000 rows and 50 columns. |
website_url_column optional | string | Which column holds the website URL. Auto-detected when omitted. |
skip_flagged_rows optional | boolean | Create the campaign without rows flagged for prohibited language, instead of failing. Default false. |
prevent_retargeting optional | boolean | Drop websites you have targeted in any previous campaign. Default false. |
Request
{
"name": "Campaign A",
"website_url_column": "site_url",
"rows": [
{
"site_url": "https://site1.com",
"business_name": "Company 1"
},
{
"site_url": "https://site2.com",
"business_name": "Company 2"
}
]
}
Response
{
"success": true,
"message": "Campaign created",
"data": {
"campaign_id": "cmp_123",
"valid_rows": 150,
"invalid_rows": 5,
"duplicate_rows": 2,
"profanity_rows_skipped": 0,
"credits_required": 300,
"credits_available": 500,
"website_url_column": "site_url",
"status": "draft"
}
}
Rows are cleaned as they are accepted: blank and malformed URLs count toward invalid_rows, and repeat websites toward duplicate_rows (example.com, www.example.com and https://example.com/ are the same target). credits_required covers the base cost of the accepted rows; enabling AI adds 1 credit per website on top.
Prohibited language. If any cell contains it, the request fails with PROFANITY_DETECTED and data.flagged_rows naming the offending row_index and column. Either clean the data and retry, or resend with "skip_flagged_rows": true to create the campaign without those rows.
Sets the sender details and message content. Moves the campaign to configured. Can be called repeatedly until the campaign is submitted; each call replaces the previous configuration.
Body parameters
| Field | Type | Description |
|---|---|---|
fields conditionally required | object | Field id to value, e.g. {"{email}": "john@test.com"}. |
field_presets conditionally required | number | A saved preset id from field_presets. Applied first; fields override it. |
enable_ai_autopersonalization optional | boolean | Write a tailored message per website from the site's own content. |
ai_instructions optional | string | What the AI should write. Required when AI is on and no preset supplies instructions. |
ai_presets optional | string | A saved AI preset id. ai_instructions overrides its instructions. |
Request
{
"field_presets": 10,
"fields": {
"{name}": "John Doe",
"{email}": "john@test.com",
"{subject}": "Manual Subject",
"{message}": "Manual message content"
},
"enable_ai_autopersonalization": true,
"ai_instructions": "Write personalized outreach",
"ai_presets": "preset_1"
}
Response
{
"success": true,
"message": "Fields saved. The campaign is ready to submit.",
"data": {
"campaign_id": "cmp_123",
"status": "configured",
"ready_to_submit": true,
"missing_required_fields": [],
"ai_enabled": true,
"ai_will_run": false,
"total_websites": 150,
"credits_required": 300,
"credits_available": 500,
"next_step": "POST /api/v1/campaigns/cmp_123/submit"
}
}
The response is a readiness report. ready_to_submit tells you whether a submit would succeed right now; when it is false, missing_required_fields lists exactly what still needs a value and message spells it out — so you never have to attempt a submit to find out. credits_required is what that submit would charge, and ai_will_run reflects whether the AI credit is included (it is not when both {subject} and {message} were set manually).
How values are resolved
When more than one source supplies the same field, the winner is decided in this order:
- Manual fields — anything in
fieldsalways wins. - AI output — fills
{subject}and{message}when they were not set manually. - Preset — supplies anything neither of the above set.
So a manual {subject} with AI enabled means the AI writes the message and your subject is kept. Set both manually and AI is skipped entirely — and you are not charged the AI credit.
Length limits
Lengths are checked after tokens are resolved, since a short template can expand into a long message. Every field has a cap; most are 100 characters, with a handful of exceptions:
| Max characters | Fields |
|---|---|
| 750 | {message} |
| 100 | Everything else — {subject}, {name}, {full_name}, {first_name}, {last_name}, {email}, {city}, {state_province}, {country}, {company_name}, {misc_value}, {title}, {name_suffix}, {middle_name}, {street_line1}, {street_line2}, {zip_postal_code}, {gender}, {language}, {website}, {user_name}, {occupation}, {inquiry_type}, {department}, {preferred_contact_method}, {timeline}, {referrer} |
| 20 | {phone} |
| 10 | {age}, {budget} |
| 3 | {phone_country_code} |
These are the same caps the campaign builder enforces in the web app, so a campaign configured either way behaves identically. Every field over its limit is reported at once:
{
"success": false,
"message": "One or more fields exceed the maximum allowed length.",
"error_code": "FIELD_LENGTH_EXCEEDED",
"data": {
"fields": [
{ "field": "{message}", "max_length": 750, "actual_length": 1241 },
{ "field": "{subject}", "max_length": 100, "actual_length": 312 }
]
}
}
Format validation
A few fields must be in a specific shape, because target forms will reject them otherwise: {email} must be a valid address, {phone_country_code} 1–3 digits with no plus sign, {phone} 7–15 digits (spaces, hyphens, parentheses, and a leading + are fine), and {website} a real domain or URL. Problems come back as INVALID_FIELD_VALUE with every offending field and a specific reason:
{
"success": false,
"message": "Invalid value for {email}, {phone_country_code}.",
"error_code": "INVALID_FIELD_VALUE",
"data": {
"fields": [
{ "field": "{email}", "message": "Must be a valid email address (e.g. name@example.com)." },
{ "field": "{phone_country_code}", "message": "Must be 1 to 3 digits with no plus sign (e.g. 1 or 44)." }
]
}
}
Renders the message for a single row exactly as it will be submitted — presets applied, tokens resolved, and AI run if enabled. Costs no credits.
Body parameters
| Field | Type | Description |
|---|---|---|
sample_row_index optional | integer | Zero-based row to preview. A random row is used when omitted. |
Request
{ "sample_row_index": 0 }
Response
{
"success": true,
"message": "",
"data": {
"sample_row_index": 0,
"website": "https://site1.com",
"subject_preview": "Question for Company 1",
"message_preview": "Hi Company 1, I'm John Doe..."
}
}
Charges credits and starts processing. Only a configured campaign can be submitted, and only once.
Body parameters
| Field | Type | Description |
|---|---|---|
webhook_url optional | string | Public http(s) URL notified when the campaign finishes. See Webhooks. |
Request
{ "webhook_url": "https://example.com/webhook" }
Response
{
"success": true,
"message": "Campaign submitted. Processing has started.",
"data": {
"campaign_id": "cmp_123",
"status": "processing",
"queued": false,
"total_websites": 150,
"credits_charged": 300,
"credits_remaining": 200,
"ai_enabled": false,
"webhook_url": "https://example.com/webhook",
"submitted_at": "2026-04-23T12:05:00Z",
"track": {
"detail": "GET /api/v1/campaigns/cmp_123",
"results": "GET /api/v1/campaigns/cmp_123/sitelevelcampaignhistory"
}
}
}
A queued status with "queued": true is normal — the campaign starts automatically when a processing slot frees up. Nothing further is required from you.
Validation errors
Each of these reports every problem at once, so one round trip is enough to fix them all:
| Error code | Meaning |
|---|---|
MISSING_REQUIRED_FIELDS | data.fields lists every required field with no value; data.fix names the call that sets them. |
INVALID_FIELD_VALUE | data.fields lists every field whose value is in the wrong format, with a reason. |
PROFANITY_DETECTED | data.fields lists every field containing prohibited language. |
INSUFFICIENT_CREDITS | data carries credits_required and credits_available. |
{
"success": false,
"message": "Cannot submit: 2 required fields have no value — {email}, {phone}. Set them with POST /api/v1/campaigns/cmp_123/fields, then submit again.",
"error_code": "MISSING_REQUIRED_FIELDS",
"data": {
"fields": [
{ "field": "{email}", "label": "Email" },
{ "field": "{phone}", "label": "Phone" }
],
"fix": "POST /api/v1/campaigns/cmp_123/fields"
}
}
{
"success": false,
"message": "Insufficient credits",
"error_code": "INSUFFICIENT_CREDITS",
"data": {
"credits_required": 300,
"credits_available": 120
}
}
Campaign controls
Pause, resume, and delete.
Pauses a processing campaign. Websites already mid-submission finish first. Credits are not refunded — they are held and consumed when you resume. Any other status returns CAMPAIGN_NOT_PROCESSING.
Response
{
"success": true,
"message": "Campaign paused",
"data": { "campaign_id": "cmp_123", "status": "paused" }
}
Resumes a paused campaign where it left off; no further charge. Any other status returns CAMPAIGN_NOT_PAUSED. The response may come back queued if a slot is not free yet.
Response — a slot was free
{
"success": true,
"message": "Campaign resumed",
"data": { "campaign_id": "cmp_123", "status": "processing" }
}
Response — waiting for a slot
{
"success": true,
"message": "Campaign queued — it will resume automatically.",
"data": { "campaign_id": "cmp_123", "status": "queued" }
}
Permanently deletes a campaign and its rows. Only campaigns that have never been submitted (draft or configured) can be deleted; anything else returns CAMPAIGN_NOT_DELETABLE.
The path is the campaign itself, with no action segment after it — unlike /pause and /resume, which act on a campaign that continues to exist. It is the same path as campaign detail, with DELETE in place of GET.
Response
{
"success": true,
"message": "Campaign deleted",
"data": { "campaign_id": "cmp_123" }
}
There is no status in this response: the campaign no longer exists, so it has no state to report. A follow-up GET /campaigns/cmp_123 returns 404 with CAMPAIGN_NOT_FOUND.
Campaign information
Listings, progress, and per-website results.
All your campaigns, newest first, with status, progress, and results — enough to build a dashboard without fetching each campaign individually. Accepts page and limit.
Response
{
"success": true,
"data": {
"data": [
{
"id": "cmp_123",
"name": "Campaign A",
"status": "processing",
"progress_percent": 65,
"total_websites": 100,
"success": 60,
"failed": 5,
"percent": 92,
"created_at": "2026-04-23T12:00:00Z"
}
],
"pagination": { "page": 1, "limit": 50, "total": 320 }
}
}
| Field | Meaning |
|---|---|
progress_percent | How far the campaign has got — websites processed as a share of total_websites. |
success | Websites whose form was submitted. |
failed | Websites that could not be submitted to. |
percent | How well it is doing — success as a share of the websites processed so far. 0 before anything has been processed. |
The two percentages answer different questions, so they rarely match: the example above is 65% of the way through, and 92% of what it has attempted has succeeded. percent here is the same measure as the webhook's percent, and success and failed match the progress block on campaign detail.
Full detail for one campaign: current status, a progress breakdown, and a tally of every error encountered. Safe to poll while the campaign runs.
Response
{
"success": true,
"data": {
"id": "cmp_123",
"name": "Campaign A",
"status": "processing",
"progress": {
"processed": 80,
"total": 100,
"success": 70,
"failed": 10,
"percent": 80
},
"errors": [
{
"type": "No contact form was found on this website.",
"error_code": "NO_CONTACT_FORM",
"count": 248
},
{
"type": "The website does not exist (its domain name could not be resolved).",
"error_code": "DOMAIN_NOT_FOUND",
"count": 145
}
],
"ai_enabled": false,
"webhook_url": "https://example.com/webhook",
"created_at": "2026-04-23T12:00:00Z",
"started_at": "2026-04-23T12:05:00Z",
"updated_at": "2026-04-23T12:50:00Z"
}
}
The errors array is always sorted from most to least frequent, and contains every error type this campaign actually produced — it is not a fixed list, so new categories can appear over time. Match on error_code rather than type.
Per-website results, paginated, updated in real time — you do not have to wait for the campaign to finish. Each record maps back to your original data through row_index. Accepts page and limit.
Row statuses
| Status | Meaning | result_details |
|---|---|---|
pending | Not processed yet. | null |
processing | Being worked on right now. | null |
success | The form was submitted. | message, fields_mapped, email_retrieved |
fail | The submission did not happen. | message, error_code, email_retrieved |
Response
{
"success": true,
"data": {
"campaign_id": "cmp_123",
"summary": {
"total": 100,
"processed": 62,
"success": 50,
"failed": 12,
"emails_retrieved_count": 13
},
"results": [
{
"row_index": 0,
"website": "https://govisiondesign.com",
"status": "success",
"result_details": {
"message": "Submitted",
"fields_mapped": 6,
"email_retrieved": [
"info@govisiondesign.com",
"sales@govisiondesign.com"
]
},
"processed_at": "2026-04-23T12:50:00Z"
},
{
"row_index": 1,
"website": "https://example.com",
"status": "fail",
"result_details": {
"message": "No contact form was found on this website.",
"error_code": "NO_CONTACT_FORM",
"email_retrieved": []
},
"processed_at": "2026-04-23T12:52:00Z"
},
{
"row_index": 2,
"website": "https://pending-example.com",
"status": "pending",
"result_details": null,
"processed_at": null
}
],
"pagination": { "page": 1, "limit": 50, "total": 100 }
}
}
email_retrieved holds any public email addresses found on the site while looking for its contact form — available on successful and failed rows alike. emails_retrieved_count in the summary counts websites that yielded at least one address.
Billing
Balance, plans, cards, and purchases.
Your current balance, the card that would be charged, and the plans available to buy.
Response
{
"success": true,
"data": {
"credits": 1200,
"default_card_id": "card_1",
"plans": [
{ "id": "plan_1", "name": "Starter", "credits": 1000, "price": 49 }
]
}
}
Buys a credit plan, charging your default card.
An Idempotency-Key header is required. Send any unique string per purchase attempt. Retrying with the same key never charges twice — the original response is replayed, whether it succeeded or was declined. Reusing a key with a different body returns 422, and retrying while the first attempt is still in flight returns 409.
Request
POST /api/v1/billing/buy
Authorization: Bearer sk_live_your_api_key_here
Idempotency-Key: unique_key_here
Content-Type: application/json
{ "plan_id": "plan_1" }
Response
{
"success": true,
"message": "Credits purchased successfully",
"data": {
"credits_added": 1000,
"new_balance": 2200,
"card_used": {
"id": "card_1",
"brand": "visa",
"last4": "4242",
"is_default": true
}
}
}
Errors
{
"success": false,
"message": "The card was declined.",
"error_code": "CARD_FAILED"
}
{
"success": false,
"message": "No default payment method found",
"error_code": "NO_DEFAULT_CARD"
}
Every saved card, with is_default marking the one /billing/buy will charge.
Response
{
"success": true,
"data": [
{ "id": "card_1", "brand": "visa", "last4": "4242", "is_default": true }
]
}
Cards are added in the dashboard, not through this API. Save or remove a card on your account's billing page, where the details go directly to Stripe. There is deliberately no endpoint for adding one — it is the same approach Stripe, AWS, and Twilio take, and it keeps cardholder data off the API entirely. The endpoints here work with cards already on your account.
Makes a saved card the default — the one /billing/buy charges. Use a card_id from GET /billing/cards.
The change takes effect immediately, so the next purchase uses the new card. Setting a card that is already the default succeeds and changes nothing, which makes the call safe to retry.
Response
{
"success": true,
"message": "Default card updated",
"data": {
"card": {
"id": "card_2",
"brand": "mastercard",
"last4": "1111",
"is_default": true
}
}
}
An unknown card_id, or one belonging to another account, returns 404 with CARD_NOT_FOUND; the card that was default before stays default.
Webhooks
Get told when a campaign finishes, instead of polling.
Pass webhook_url when you submit a campaign and we will POST this JSON to it once the campaign reaches completed or failed:
{
"campaign_id": "cmp_123",
"status": "completed",
"processed": 100,
"success": 90,
"failed": 10,
"percent": 90
}
percent is the success rate — success as a share of processed, rounded to a whole number, and 0 if nothing was processed. It is not the same as progress.percent on campaign detail, which measures how far a campaign got rather than how well it did; by the time this webhook fires, that one is always 100.
| Method | POST with Content-Type: application/json |
| User agent | ContactFormBlaster-Webhook/1.0 |
| Success | Any 2xx response, within 10 seconds |
| Retries | 3 further attempts, after 5s, 30s, and 2 minutes |
| Delivery | At most once per campaign |
The URL must be publicly reachable over http or https; private, loopback, and internal addresses are rejected at submit time with INVALID_WEBHOOK_URL.
Treat webhooks as a convenience, not a guarantee. If your endpoint is down for all four attempts the notification is dropped. Campaign detail is always the source of truth.
Error codes
Every code the API can return, and what to do about it.
Authentication & limits
| Code | HTTP | Meaning |
|---|---|---|
UNAUTHORIZED | 401 | Missing, malformed, or revoked API key. |
ACCOUNT_SUSPENDED | 403 | The account is suspended. Contact support. |
EMAIL_NOT_VERIFIED | 403 | Verify the account's email address first. |
RATE_LIMIT_EXCEEDED | 429 | Over 60 requests this minute. Wait Retry-After seconds. |
TOO_MANY_AUTH_FAILURES | 429 | Too many invalid keys sent from this IP in a minute. Check the key you are sending. |
API_KEY_NOT_FOUND | 404 | No API key exists for this account. |
API_KEY_GENERATION_FAILED | 500 | The key could not be generated. Retry. |
Campaigns
| Code | HTTP | Meaning |
|---|---|---|
CAMPAIGN_NOT_FOUND | 404 | Unknown campaign id, or it is not yours. |
INVALID_NAME | 400 | Missing name, or longer than 120 characters. |
INVALID_ROWS | 400 | rows is missing, empty, or not an array of objects. |
TOO_MANY_ROWS | 400 | More than 30,000 rows. |
TOO_MANY_COLUMNS | 400 | More than 50 columns. |
INVALID_URL_COLUMN | 400 | The named website_url_column is not in the data. |
URL_COLUMN_NOT_FOUND | 400 | No column of website URLs could be detected. Name it explicitly. |
DAILY_LIMIT_REACHED | 429 | Too many campaigns created in 24 hours. |
PROFANITY_DETECTED | 400 | Prohibited language. data lists every occurrence. |
INVALID_FIELDS | 400 | The fields request had nothing to save, or fields is not an object. |
UNKNOWN_FIELD | 400 | A field id that does not exist. Check GET /form-fields. |
INVALID_FIELD_VALUE | 400 | A field value is in the wrong format (email, phone, country code, website). All listed in data. |
UNKNOWN_TOKEN | 400 | A {token} not available to this campaign. |
TOKENS_NOT_ALLOWED | 400 | A token used outside {subject} / {message}. |
FIELD_LENGTH_EXCEEDED | 400 | Resolved subject or message too long. All listed in data. |
PRESET_NOT_FOUND | 400 | No field preset with that id. |
AI_PRESET_NOT_FOUND | 400 | No AI preset with that id. |
MISSING_AI_INSTRUCTIONS | 400 | AI is enabled with no instructions to work from. |
CAMPAIGN_NOT_EDITABLE | 400 | Fields can only be set before submission. |
CAMPAIGN_NOT_CONFIGURED | 400 | Set fields before previewing or submitting. |
CAMPAIGN_ALREADY_SUBMITTED | 400 | This campaign has already been submitted. |
MISSING_REQUIRED_FIELDS | 400 | Required fields have no value. All listed in data. |
INSUFFICIENT_CREDITS | 400 | Not enough credits. data has required vs. available. |
NO_ROWS | 400 | The campaign has no valid rows. |
INVALID_ROW_INDEX | 400 | sample_row_index is outside the campaign's rows. |
INVALID_WEBHOOK_URL | 400 | webhook_url is not a public http(s) URL. |
CAMPAIGN_NOT_PROCESSING | 400 | Only a processing campaign can be paused. |
CAMPAIGN_NOT_PAUSED | 400 | Only a paused campaign can be resumed. |
CAMPAIGN_NOT_DELETABLE | 400 | Only an unsubmitted campaign can be deleted. |
AI_PREVIEW_FAILED | 500 | The AI preview could not be generated. Retry. |
AI_UNAVAILABLE | 503 | The AI service is busy. Retry shortly. |
Billing
| Code | HTTP | Meaning |
|---|---|---|
IDEMPOTENCY_KEY_REQUIRED | 400 | /billing/buy called without an Idempotency-Key header. |
IDEMPOTENCY_KEY_REUSED | 422 | That key was already used for a different request body. |
REQUEST_IN_PROGRESS | 409 | A purchase with that key is still being processed. |
INVALID_PLAN | 400 | No such plan. Check GET /billing/credits. |
NO_DEFAULT_CARD | 402 | No saved card to charge. |
CARD_FAILED | 402 | The card could not be charged. |
CARD_NOT_FOUND | 404 | No card with that id on this account. Check GET /billing/cards. |
Request & server
| Code | HTTP | Meaning |
|---|---|---|
NOT_FOUND | 404 | No such endpoint. Check the method and path. |
INVALID_JSON | 400 | The request body is not valid JSON. |
PAYLOAD_TOO_LARGE | 413 | The request body exceeds 50 MB. |
SERVICE_UNAVAILABLE | 503 | Processing is paused for maintenance. Retry shortly. |
INTERNAL_ERROR | 500 | Something failed on our side. Safe to retry. |