API reference — LoopNow developer docs
LoopNow REST API: 8 endpoints (campaigns, contacts, automations, transactional, analytics, webhooks). Bearer auth, idempotency keys, cursor pagination, error codes.
On this page
Authentication
All endpoints require a Bearer token. Get your token from Settings → API keys in the LoopNow dashboard. The token is scoped to a single workspace.
curl https://api.loopnow.in/v1/contacts \
-H "Authorization: Bearer ln_live_xxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json"
API keys are prefixed by environment: ln_live_ for production, ln_test_ for the sandbox. The sandbox is a fully separate workspace — no production data is visible or modifiable.
Rotate keys at any time from the dashboard. The old key remains valid for 24 hours after rotation, then is revoked. This is to give in-flight requests time to complete.
Error codes
The API uses standard HTTP status codes. The body of every error response is:
{
"error": {
"code": "invalid_request",
"message": "The 'to' field is required.",
"param": "to",
"request_id": "req_abc123"
}
}
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | Malformed request, missing or invalid parameter |
| 401 | unauthorized | Missing or invalid Bearer token |
| 403 | forbidden | Token does not have scope for this operation |
| 404 | not_found | Resource does not exist or is not in this workspace |
| 409 | conflict | Idempotency key already used with a different request body |
| 422 | unprocessable | Request was well-formed but semantically invalid (e.g. contact already exists, list is archived) |
| 429 | rate_limited | You exceeded the rate limit. See Retry-After header. |
| 500 | server_error | Something went wrong on our end. Safe to retry with the same idempotency key. |
Always log the request_id — it is the single most useful piece of information when you contact support.
Rate limits
Rate limits are per-token, sliding 60-second window:
- Read endpoints (GET): 600 requests / minute
- Write endpoints (POST, PUT, DELETE): 300 requests / minute
- Transactional send: 10,000 requests / minute (with a per-second burst of 200)
When you hit a limit, the response is HTTP 429 with a Retry-After header (in seconds) and a X-RateLimit-Reset header (Unix timestamp). Respect them.
Pagination
List endpoints return a data array and a page object. The page object has next_cursor if there is more data; pass that as the cursor query parameter to get the next page.
GET /v1/contacts?cursor=cD0yMDI2LTAxLTE0
{
"data": [ ... up to 100 contacts ... ],
"page": {
"next_cursor": "cD0yMDI2LTAxLTE1",
"has_more": true
}
}
Default page size is 50; max is 100. Pass limit to override. There is no offset pagination — cursors are stable across inserts but not across deletes (a deleted item shifts the cursor forward).
Idempotency keys
For any write endpoint that creates a side effect (sending a campaign, creating a contact, triggering an automation), you can pass an Idempotency-Key header. The API will return the same response for the same key within a 24-hour window, even if the underlying operation is retried.
POST /v1/transactional/send
Idempotency-Key: my-order-12345-confirmation
Use this for any operation that your client may retry on network failure — especially transactional sends. Without an idempotency key, a retry on a network failure can result in two emails going out.
Endpoints
Below are the 8 most-used endpoints. The full list (40+ endpoints) is available at https://api.loopnow.in/v1/openapi.json as an OpenAPI 3.1 document.
POST /v1/campaigns
Create a campaign. The campaign is in draft state until you call /send.
POST /v1/campaigns
{
"name": "January newsletter",
"from": { "email": "hello@yourdomain.in", "name": "Your Brand" },
"subject": "What shipped in January",
"preview_text": "3 product updates, 2 case studies, 1 customer story.",
"audience_id": "aud_abc123",
"content": {
"type": "template",
"template_id": "tpl_newsletter_v3"
}
}
Response 201:
{
"id": "cmp_xyz789",
"status": "draft",
"created_at": "2026-01-14T10:30:00Z"
}
POST /v1/campaigns/{id}/send
Send a campaign. The audience is the one set at creation time. Schedule for the future with send_at.
POST /v1/campaigns/cmp_xyz789/send
{
"send_at": null
}
Response 202:
{
"id": "snd_def456",
"campaign_id": "cmp_xyz789",
"status": "queued",
"queued_for": "2026-01-14T10:31:00Z"
}
GET /v1/contacts
List contacts in the workspace. Filter by list, tag, custom field, or consent status.
GET /v1/contacts?list_id=aud_abc123&tag=vip&limit=50
The response includes a signed download URL for any contact that has a custom field of type file. The URL is valid for 15 minutes.
POST /v1/contacts
Create or update a contact (upsert by default). Pass idempotency_key in the body to make the operation idempotent.
POST /v1/contacts
{
"email": "user@example.com",
"first_name": "Riya",
"last_name": "Shah",
"list_ids": ["aud_abc123"],
"tags": ["vip", "early-access"],
"consent": {
"source": "homepage_signup",
"ip": "203.0.113.42",
"text": "I agree to receive marketing emails."
}
}
The consent object is the DPDP-relevant one. It writes a tamper-evident consent receipt at write time, with the exact text shown, the source URL, the IP, and the user agent. You cannot create a contact without consent on a paid plan; the field is required.
POST /v1/automations/trigger
Trigger an automation for a specific contact. Useful for "user did X" events from your own application.
POST /v1/automations/trigger
{
"automation_id": "aut_welcome_series",
"contact_email": "user@example.com",
"context": {
"plan": "scale",
"trial_days": 14
}
}
The context object becomes available to all email templates in the automation as merge variables. Use this to pass the user's selected plan, trial end date, or any other custom data.
GET /v1/analytics/campaigns/{id}
Get delivery, open, click, bounce, and complaint stats for a sent campaign.
GET /v1/analytics/campaigns/cmp_xyz789
Response:
{
"campaign_id": "cmp_xyz789",
"sent": 12450,
"delivered": 12289,
"opens": 5421,
"unique_opens": 4102,
"clicks": 982,
"unique_clicks": 811,
"bounces": 161,
"complaints": 4,
"unsubscribes": 18,
"open_rate": 0.334,
"click_rate": 0.066,
"complaint_rate": 0.0003
}
Stats are updated every 60 seconds. For real-time stats, subscribe to the webhooks instead.
POST /v1/transactional/send
Send a one-off transactional email. Use this for password resets, order confirmations, OTPs, etc. Bypasses the campaign audience and sends to the address you specify.
POST /v1/transactional/send
{
"to": "user@example.com",
"from": { "email": "no-reply@yourdomain.in", "name": "Your Brand" },
"subject": "Your order #1234 is confirmed",
"html": "<h1>Thanks for your order</h1>...",
"metadata": {
"order_id": "1234"
},
"idempotency_key": "order-1234-confirmation"
}
The metadata object is echoed back in webhooks for transactional events, so you can correlate a bounce or complaint with the originating order.
GET /v1/webhooks
List registered webhook endpoints and their subscribed event types. See the webhooks documentation for event types and signature verification.
GET /v1/webhooks
{
"data": [
{
"id": "whk_aaa111",
"url": "https://yourapp.com/webhooks/loopnow",
"events": [
"contact.created",
"campaign.sent",
"transactional.delivered",
"transactional.bounced"
],
"created_at": "2026-01-10T08:00:00Z",
"last_success_at": "2026-01-14T10:30:00Z"
}
]
}
Versioning and changelog
The API is versioned in the URL: /v1/. Breaking changes are introduced in a new major version (/v2/) with a 6-month deprecation window for the old version. Non-breaking additions (new fields on responses, new optional parameters) are added to the current version without a version bump.
The full changelog is at /changelog. Subscribe via RSS or email to be notified of API changes.
OpenAPI document
The full API surface, including the 30+ endpoints not documented on this page, is published as an OpenAPI 3.1 document at https://api.loopnow.in/v1/openapi.json. You can use this to generate client libraries in any language that has an OpenAPI generator (TypeScript, Go, Rust, Java, Kotlin, Swift, etc).
Postman collection
A maintained Postman collection with all 40+ endpoints and example payloads is at https://api.loopnow.in/v1/postman.json. Import it into Postman or any compatible client (Insomnia, Bruno, Hoppscotch).
Status codes reference
The full list of HTTP status codes returned by the API, with the loopnow-specific error codes that may appear in the response body:
| Status | loopnow code | Meaning |
|---|---|---|
| 400 | invalid_request | Malformed request, missing or invalid parameter |
| 400 | invalid_email | The email address is malformed |
| 400 | consent_required | The consent object is missing or incomplete |
| 401 | unauthorized | Missing or invalid Bearer token |
| 401 | token_expired | The Bearer token has been revoked |
| 403 | forbidden | Token does not have scope for this operation |
| 404 | not_found | Resource does not exist or is not in this workspace |
| 409 | conflict | Idempotency key already used with a different request body |
| 422 | unprocessable | Request was well-formed but semantically invalid |
| 422 | already_exists | Resource already exists (e.g. duplicate contact with conflicting fields) |
| 429 | rate_limited | You exceeded the rate limit |
| 500 | server_error | Something went wrong on our end. Safe to retry. |
| 503 | maintenance | Service is in maintenance mode. Retry with backoff. |
What is next
- Webhooks — subscribe to events, verify signatures.
- Python SDK — wraps the API in idiomatic Python.
- Node SDK — TypeScript types included.
- SMTP relay — if you cannot use the API.