User Guide · v1.0

Email Campaign & Delivery Tracker — User Guide

A step-by-step manual for building a contact list, sending a campaign, and reading exactly what happened to every recipient — queued, delivered, opened, clicked, bounced, or unsubscribed — for every role on the platform, analyst through platform admin.

Publisher
Arsi India Info
Web App
https://demo1.arsiindiainfo.com
API Base URL
https://demo1.arsiindiainfo.com/api/v1
Interactive Docs
Local dev only — see §3
Audience
Analysts · Marketers · Admins · Owners
4
User Roles
6
Campaign States
9
Recipient Statuses
1
ESP Integrated (SES)

1. Welcome

This platform is a fictional email marketing tool for NovaMail Retail Co. — build a contact list, write a template, send a campaign, and watch delivery events (sent, delivered, opened, clicked, bounced, complained, unsubscribed) roll in on the analytics page in real time. Every send in this demo goes through Mailhog/a sandboxed queue in non-production environments — no real email is ever sent to a real inbox outside of `production`.

Who this guide is for Anyone using or evaluating this platform — no prior knowledge of the backend architecture is required. Most people should just use the web app (§2); developers calling the API directly can skip ahead to §3.

2. Two Ways to Use This Platform

There's a real, working web application in front of this platform — you don't need to write any code or run any HTTP requests by hand to try it out.

Web App Recommended

Open https://demo1.arsiindiainfo.com, sign in (§6), and click through the Dashboard, Lists, Templates, Campaigns, Suppressions, and Settings screens — plus three Admin screens if you're a platform admin. §5 breaks down exactly which screens each role sees.

Raw API / Swagger

Everything the web app does, it does by calling the same JSON API documented in §3 onward. Use this if you're integrating programmatically, testing with curl/Postman, or want exact request/response shapes via the interactive Swagger UI (local dev only).

This guide covers both Every workflow section from §9 onward gives you the web-app steps first ("Via the Web App") and the equivalent raw HTTP call underneath ("Via the API") — use whichever matches how you're exploring the platform.

3. Web App & API Reference

Everything is reachable from one public API base URL — there is exactly one entry point into the platform.

ResourceURL
Web apphttps://demo1.arsiindiainfo.com
API base URLhttps://demo1.arsiindiainfo.com/api/v1
Interactive Swagger docsGET/api/docs (non-production environments only)
Public tracking pixelGET /t/o/:token — unversioned, embedded in a sent email
Public click redirectGET /t/c/:token — unversioned, records a click then 302s to the real link
Public one-click unsubscribeGET /api/v1/unsubscribe/:token

Response envelope

Every response — success or failure — is wrapped the same way, so client code only ever needs one shape to check:

// success
{
  "success": true,
  "data": { /* the actual result */ }
}

// list responses also include a "meta" block
{
  "success": true,
  "data": [ /* rows */ ],
  "meta": { "page": 1, "limit": 20, "total": 8 }
}

// failure
{
  "success": false,
  "error": { "code": "VALIDATION_ERROR", "message": "..." }
}

4. Demo Accounts & Roles

The seed script creates the fictional NovaMail Retail Co. organization with one account per role. Unlike a self-contained demo dataset, this platform is a real public multi-tenant SaaS — anyone can register their own organization (§7) — so the seeded accounts' credentials aren't published here; ask whoever manages this deployment for sign-in access, or register your own account to explore as an Owner immediately.

RoleRankCan do
Owner3 (highest)Everything a org member can do, plus the seeded account is also the platform's one Platform Admin (§17–19) — a separate, cross-organization flag set only via seed/ops, never through any endpoint.
Admin2Everything Marketer can, plus organization-level settings (§16).
Marketer1Create/edit/send campaigns and templates, manage lists.
Analyst0Read-only — views the Dashboard, campaigns, and analytics, but can't create or send anything.
Demo abuse guardrails Because registration is open to anyone, non-production environments cap total sends per organization (DEMO_SEND_QUOTA_EXCEEDED) and only allow sending to *.demo-style addresses or your own verified sender domain (DEMO_RECIPIENT_NOT_ALLOWED) — see §18–19 for how a platform admin monitors this.

5. Which Screen Is For Which Role

Every signed-in user sees the same core navigation. An Analyst sees every screen below except the three write actions noted; a Owner who is also the platform admin additionally sees three screens under "Admin" in the sidebar.

ScreenAnalystMarketer+What it's for
DashboardKPI row + recent campaigns table (§9).
Lists✅ view✅ create/importRecipient lists and their contacts (§10).
Templates✅ view✅ create/editReusable email bodies with merge fields (§11).
Campaigns✅ view✅ create/sendThe full lifecycle from draft to sent (§12–13).
Campaign analyticsFunnel + time series + per-recipient status (§14).
Suppressions✅ view✅ add/removeAddresses that will never receive another send (§15).
Settings✅ Admin+ onlySender domain verification (§16).
Admin: UsersPlatform admin only — every user, across every org (§17).
Admin: ContactsPlatform admin only — approve/reject imported contacts (§18).
Admin: Send LogPlatform admin only — every demo send, across every org (§19).

6. Signing In

The sign-in page asks for your email, password, and a reCAPTCHA check.

Via the Web App

  1. Open https://demo1.arsiindiainfo.com — you land on the sign-in page.
  2. Enter your email and password.
  3. Tick the reCAPTCHA checkbox — see the callout below.
  4. Click Sign in. You land on the Dashboard (§9).
Sign-in page for the Email Campaign Tracker
The sign-in page.
Why there's a reCAPTCHA checkbox POST /auth/login and POST /auth/register are the only endpoints anyone can call without already being logged in — the obvious targets for scripted/credential-stuffing traffic on a publicly-registrable platform. Both require a Google reCAPTCHA v2 check verified server-side before the request is accepted.

Via the API

POST https://demo1.arsiindiainfo.com/api/v1/auth/login
Content-Type: application/json

{
  "email": "you@example.com",
  "password": "...",
  "recaptchaToken": "03AGdBq27..."
}
{
  "success": true,
  "data": {
    "accessToken": "eyJhbGciOi...",
    "refreshToken": "9ba3b6cf3c...",
    "user": { "id": "...", "name": "...", "email": "...", "role": "OWNER", "organizationId": "..." },
    "organization": { "id": "...", "name": "NovaMail Retail Co.", "slug": "...", "senderVerified": true }
  }
}

Copy the accessToken and send it on every subsequent request as a bearer token:

Authorization: Bearer eyJhbGciOi...

7. Creating Your Own Organization

Unlike a fixed demo dataset, this platform is a genuine multi-tenant product — anyone can register, becoming the Owner of a brand-new, empty organization.

Via the Web App

  1. Open the sign-in page and click Create an account.
  2. Fill in your organization's name, your own name, email, and password, and complete the reCAPTCHA.
  3. Submit — you're sent a verification email (Mailhog in non-production) rather than being signed in immediately.
  4. Click the link in that email (or open /verify-email/:token directly) to activate the account, then sign in (§6).
Create your organization page
Creating a new organization.
No tokens until you verify POST /auth/register never returns an accessToken — the account isn't usable until POST /auth/verify-email/:token succeeds. Signing in beforehand returns 403 EMAIL_NOT_VERIFIED.

Via the API

POST/auth/register{ organizationName, name, email, password, recaptchaToken }. A duplicate email returns 409 DUPLICATE_NAME.

POST/auth/verify-email/:token — an invalid/expired token returns 400 INVALID_VERIFICATION_TOKEN.

8. Your Session & Signing Out

Access tokens are short-lived. The web app quietly exchanges the refreshToken for a new pair in the background whenever a request comes back 401 — you won't see a token expire while you're using the app.

POST/auth/refresh{ refreshToken }. POST/auth/logout revokes the refresh token so it can no longer be used.

Via the Web App: click Sign out in the sidebar. Via the API: GET/users/me returns your own profile.

9. The Dashboard

The Dashboard is the landing page after sign-in — a KPI row and your organization's most recent campaigns.

Via the Web App: KPI cards (total sent, delivery rate, open rate, click rate) and a recent-campaigns table with each one's status pill.

Dashboard with KPI row and recent campaigns table
The Dashboard — signed in as Owner for NovaMail Retail Co.

Via the API: GET/analytics/overview returns the same numbers scoped to your organization.

10. Contact Lists & Importing

A campaign always sends to exactly one list. Lists hold contacts; a contact can belong to more than one list.

Via the Web App

  1. Open Lists — click + New List to create one, or click any row to open its detail page.
  2. On a list's detail page, add a contact one at a time, or click Import CSV to bulk-upload (name/email columns).
  3. A contact who bounced or complained on any send shows an auto-suppressed badge — see §15, it's platform-wide, not per-list.
Recipient lists — name, contact count, created date
Recipient lists.
A list's contacts, one shown auto-suppressed after a bounce
A list's detail page — note the auto-suppressed contact.
Creating a new list — name field, Create/Cancel
Creating a new list.

Via the API

POST/lists{ name }. POST/lists/:id/contacts — add one contact directly.

Bulk import is a three-step, presigned-upload flow (§20 covers the same pattern for webhooks):

  1. POST/lists/:id/imports/presign — get a short-lived upload URL for your CSV.
  2. Upload the file straight to that URL (never through this API).
  3. POST/lists/:id/import — kick off processing; poll GET/lists/:id/imports/:jobId for progress.

11. Templates & the Editor

A template is the reusable email body a campaign sends — subject line, HTML body, and merge fields like {{firstName}}.

Via the Web App

  1. Open Templates — click + New Template or click any row to edit.
  2. The editor has merge-field shortcut buttons and a live preview pane rendered with sample contact data.
  3. Every template needs at least one unsubscribe merge tag before it can be used in a send — the editor warns you if it's missing.
Templates list
The Templates list.
Template editor with merge-field shortcuts and a live preview pane
The template editor — merge fields on the left, live preview on the right.

Via the API

POST/templates · GET/templates · PUT/templates/:id · DELETE/templates/:id. A missing/unresolvable template on a campaign returns 404 TEMPLATE_NOT_FOUND.

12. Creating a Campaign

The campaign wizard ties a list and a template together, with your organization's verified sender applied automatically.

Via the Web App

  1. Open Campaigns and click + New Campaign.
  2. Name the campaign, pick a list and a template — your org's sender domain (§16) is applied without asking.
  3. Save — the campaign starts in Draft. It isn't sent until §13.
New campaign wizard, step 1: Details
Step 1 — Details. The From email must use your verified sender domain (§16).
New campaign wizard, step 2: Recipients
Step 2 — Recipients. Pick one or more lists; the wizard totals the recipient count.
New campaign wizard, step 3: Content, with a live preview
Step 3 — Content. Pick a template; the preview renders it with sample data.
New campaign wizard, step 4: Review and Schedule
Step 4 — Review & Schedule (§13) — Send now, or pick a future time.
Optimistic locking Every campaign carries a version number. Open the same draft in two tabs, edit and save in one, then try to save a stale edit in the other — the second save returns 409 VERSION_CONFLICT instead of silently overwriting the first save.
A stale-edit save attempt returning a version-conflict error banner
Exactly that — a second tab's stale save gets caught, not silently applied.

Via the API

POST/campaigns{ name, listId, templateId }. PUT/campaigns/:id{ ..., version }; a stale version returns 409 VERSION_CONFLICT.

13. Sending — the Delivery Pipeline

A campaign moves through six states; a per-recipient row inside it moves through its own, longer pipeline.

Campaign lifecycle

1
Draft
Being edited, never sent
2
Scheduled
Queued for a future time
3
Sending
Worker dispatching recipients now
4
Sent
Every recipient dispatched
Paused
Mid-send, resumable
3
Sending
Resume continues where it left off
Cancelled
Not-yet-dispatched recipients are skipped for good

Via the Web App

  1. Open a Draft campaign and click Schedule — pick a future time, or choose Send now for an immediate queue.
  2. While Sending, click Pause/Resume to hold or continue dispatch, or Cancel to stop it for good.
  3. The campaign flips to Sent once every recipient has been dispatched.

Per-recipient delivery pipeline

1
Queued
2
Sent
3
Delivered
4
Opened
5
Clicked
Bounced
Auto-suppressed
Complained
Auto-suppressed
Failed
Unsubscribed
One-click, idempotent
Forward-only, never downgraded A webhook that arrives out of order (e.g. a delayed "delivered" event after the recipient already opened it) never moves a recipient backward — each status carries a forward-progress rank, and an out-of-rank event is recorded but doesn't change the recipient's current status. The four side-branches above are terminal: nothing moves a recipient out of them.

Watching it happen: the analytics page (§14) polls every 15 seconds while a campaign is Sending, so the funnel and per-recipient table update live as delivery events arrive.

Via the API

POST/campaigns/:id/schedule{ sendAt? }; omit sendAt (or set it to now) for an immediate send. POST/campaigns/:id/pause · POST/campaigns/:id/resume · POST/campaigns/:id/cancel. POST/campaigns/:id/send-test{ emails: string[] }, up to 5 addresses, doesn't touch the real recipient list. GET/campaigns/:id/recipients — every recipient's current status.

Closed states don't reopen Any transition attempt outside the arrows above returns 409 INVALID_STATE_TRANSITION — you can't schedule a Cancelled campaign, or pause one that's already Sent.

14. Campaign Analytics

Every number here is derived from the append-only event log described in §13 — nothing is a running counter that could drift.

Via the Web App: open a campaign to see its funnel (Queued → Sent → Delivered → Opened → Clicked, with Bounced/Complained side-counts), an opens/clicks time-series chart, and the per-recipient status table.

Via the API: GET/analytics/campaigns/:id returns the funnel counts and time series as one payload.

15. Suppressions

An address on the suppression list will never receive another send from this organization — added automatically on a bounce/complaint/unsubscribe, or manually.

Suppressions list, empty state
Suppressions — populated rows show why each address landed here (bounce, complaint, unsubscribe, or manual).
Suppressing an address manually
Adding one manually.

Via the API: GET/suppressions · POST/suppressions — add one manually. DELETE/suppressions/:id — remove one (use sparingly; it doesn't undo why they were suppressed).

A campaign send silently skips any recipient already on this list — no error, they're just not queued, so re-sending to a list with suppressed contacts is always safe.

16. Settings & Sender Domain

A campaign can't be scheduled until your organization's sender domain is verified. Settings has three tabs: Organization (sender domain/email), Team (your org's own members, distinct from the platform-wide Admin: Users in §17), and API / Webhooks (§20).

Via the Web App: open Settings (Admin role or above) to see your sender domain/email and its verification status; click Save to update it.

Settings page with sender domain verification
Settings — Organization tab, sender domain verification.
Settings — Team tab listing this organization's members and roles
Settings — Team tab, this organization's own members (§4's roles).

Via the API: GET/organizations/me · PUT/organizations/me · POST/organizations/me/verify-sender. Scheduling a campaign for an unverified sender returns 422 SENDER_NOT_VERIFIED.

17. Admin: Users

Sign in as the platform admin (§4) to see every user across every organization on the platform — not just your own. These endpoints return 403 FORBIDDEN_ROLE for anyone else.

Admin Users screen — every user across every organization
Admin: Users — cross-organization, deliberately bypassing the usual org-scoped view.

Via the API: GET/admin/users · PATCH/admin/users/:id/block · PATCH/admin/users/:id/unblock · DELETE/admin/users/:id. A blocked/deleted platform admin account is refused outright, so the platform can never end up with zero admins.

18. Admin: Contacts

Because anyone can register and import a contact list, every imported contact starts Pending until a platform admin approves or rejects it — the abuse guardrail mentioned in §4.

Via the Web App: filter by Pending/Approved/Rejected, and click Approve or Reject on any row.

Admin Contacts screen — imported contacts awaiting approval
Admin: Contacts — the moderation queue for imported recipients.

Via the API: GET/admin/contacts?status= · PATCH/admin/contacts/:id/approve · PATCH/admin/contacts/:id/reject. A campaign only ever sends to Approved contacts.

19. Admin: Send Log

Every demo send, across every organization, in one place — how a platform admin watches for abuse of the shared send quota (§4).

Admin Send Log — every demo send across every organization
Admin: Send Log.

Via the API: GET/admin/send-log.

20. Webhooks & Public Tracking

Delivery events arrive two ways, both outside the normal bearer-token API:

Settings — API / Webhooks tab showing the inbound webhook URL
Settings → API / Webhooks — the URL to configure as your SES/SNS delivery-notification destination.

Unsubscribe: GET/unsubscribe/:token is public and idempotent — clicking it twice is a no-op success the second time, never an error.

21. Error Responses

HTTPCodeMeaning
400VALIDATION_ERRORThe request body failed validation.
400INVALID_VERIFICATION_TOKENAn expired/invalid email-verification token.
400RECAPTCHA_FAILEDThe reCAPTCHA check didn't pass — complete the checkbox again.
401UNAUTHORIZEDMissing/expired token or invalid credentials.
401INVALID_WEBHOOK_SIGNATUREThe inbound webhook's HMAC signature didn't match (§20).
403FORBIDDEN_ROLEYour account's role/rank can't perform this action.
403EMAIL_NOT_VERIFIEDSign in attempted before verifying the account (§7).
403ACCOUNT_BLOCKEDA platform admin has blocked this account (§17).
404CAMPAIGN_NOT_FOUND / TEMPLATE_NOT_FOUND / LIST_NOT_FOUND / CONTACT_NOT_FOUND / USER_NOT_FOUND / SUPPRESSION_NOT_FOUND / IMPORT_JOB_NOT_FOUNDDoesn't exist, or belongs to another organization (tenant isolation, §7).
409DUPLICATE_NAMEA campaign/template/list/email that must be unique already exists.
409VERSION_CONFLICTA stale version on a campaign edit (§12).
409INVALID_STATE_TRANSITIONThe campaign-status change isn't allowed from its current state (§13).
422SENDER_NOT_VERIFIEDScheduling a send before sender-domain verification (§16).
422SUPPRESSED_RECIPIENTManually adding a suppressed address back to a list-based action that doesn't allow it.
429RATE_LIMITEDToo many requests — slow down and retry.
429DEMO_SEND_QUOTA_EXCEEDEDThis organization's non-production send quota is used up (§4).
422DEMO_RECIPIENT_NOT_ALLOWEDNon-production sends are restricted to demo-style/verified addresses (§4).
500INTERNAL_ERRORAn unexpected server-side error.

22. Full Endpoint Reference

All paths (except the public tracking/unsubscribe/webhook routes noted) are relative to https://demo1.arsiindiainfo.com/api/v1.

MethodPathAuthDescription
POST/auth/registerPublic + reCAPTCHARegister a new organization (§7)
POST/auth/verify-email/:tokenPublicActivate a registered account
POST/auth/loginPublic + reCAPTCHASign in (§6)
POST/auth/refreshPublicExchange a refresh token for a new pair
POST/auth/logoutBearerRevoke a refresh token
GET/users/meBearerYour own profile
GET/usersAdmin+List your organization's users
POST/users/inviteAdmin+Invite a teammate
GET/organizations/meBearerYour organization's profile
PUT/organizations/meAdmin+Update sender email/domain (§16)
POST/organizations/me/verify-senderAdmin+(Re-)check sender-domain verification
GET/listsBearerList your organization's contact lists
POST/listsMarketer+Create a list
GET/lists/:idBearerGet one list
DELETE/lists/:idMarketer+Delete a list
GET/lists/:id/contactsBearerList a list's contacts
POST/lists/:id/contactsMarketer+Add one contact
DELETE/lists/:id/contacts/:contactIdMarketer+Remove a contact from a list
POST/lists/:id/imports/presignMarketer+Get a presigned CSV upload URL (§10)
POST/lists/:id/importMarketer+Start processing an uploaded CSV
GET/lists/:id/imports/:jobIdBearerPoll an import job's progress
GET/templatesBearerList templates
POST/templatesMarketer+Create a template
GET/templates/:idBearerGet one template
PUT/templates/:idMarketer+Update a template
DELETE/templates/:idMarketer+Delete a template
GET/campaignsBearerList campaigns
POST/campaignsMarketer+Create a campaign (§12)
GET/campaigns/:idBearerGet one campaign
PUT/campaigns/:idMarketer+Update a draft campaign
DELETE/campaigns/:idMarketer+Delete a draft campaign
POST/campaigns/:id/send-testMarketer+Preview to up to 5 test addresses
POST/campaigns/:id/scheduleMarketer+Schedule or send now (§13)
POST/campaigns/:id/pauseMarketer+Pause a Sending campaign
POST/campaigns/:id/resumeMarketer+Resume a Paused campaign
POST/campaigns/:id/cancelMarketer+Cancel not-yet-dispatched sends
GET/campaigns/:id/recipientsBearerPer-recipient delivery status
GET/analytics/overviewBearerDashboard KPI row (§9)
GET/analytics/campaigns/:idBearerFunnel + time series for one campaign (§14)
GET/suppressionsBearerList suppressed addresses (§15)
POST/suppressionsMarketer+Add one manually
DELETE/suppressions/:idMarketer+Remove one
GET/admin/usersPlatform adminEvery user, every organization (§17)
PATCH/admin/users/:id/blockPlatform adminBlock a user
PATCH/admin/users/:id/unblockPlatform adminUnblock a user
DELETE/admin/users/:idPlatform adminDelete a user
GET/admin/contactsPlatform adminImported contacts awaiting moderation (§18)
PATCH/admin/contacts/:id/approvePlatform adminApprove a contact
PATCH/admin/contacts/:id/rejectPlatform adminReject a contact
GET/admin/send-logPlatform adminEvery demo send, every organization (§19)
POST/webhooks/sesHMAC-signedInbound SES/SNS delivery events (§20)

23. FAQ & Troubleshooting

The reCAPTCHA checkbox won't check, or my login/registration is rejected

Make sure third-party content isn't blocked by a privacy extension — the widget loads a script from google.com. If the checkbox works but submission still fails, the token likely expired (valid for about two minutes) — the form resets the widget automatically; complete it again and resubmit.

I registered but never got a verification email

In non-production environments, verification emails go to Mailhog, not a real inbox — ask whoever manages this deployment where to view it. The account is genuinely unusable (§7) until that link is clicked.

I sent a campaign but the recipient count is lower than my list's contact count

Two reasons this is expected: suppressed addresses (§15) are silently skipped, and imported contacts that haven't been approved yet by a platform admin (§18) never receive a send.

Why can't I resume a Cancelled campaign?

Cancelled is terminal by design (§13) — Paused is the only "stopped but resumable" state. Create a new campaign instead.

Can I make myself a platform admin?

No — isPlatformAdmin is set only via seed/ops, never through any endpoint or UI action, by design.

Where do I see the full request/response schema for every field?

The interactive Swagger UI at /api/docs documents every field, validation rule, and example — it's disabled on the public demo (ENVIRONMENT === 'production' turns it off), so run the project locally (see the repo's README) to browse it.

Arsi India Info logo
Need more detail? This guide covers the day-to-day workflows. Schema, event sourcing, and API contract details are documented separately in the project's docs/ folder (data model, deployment, testing).