NevTan Engage

Features

Email MarketingSMS MarketingAutomations

Solutions

eCommerceSaaS & AppsB2B Lead GenAgencies

Resources

BlogHelp CenterDocumentationEmail TemplatesAPIFAQs

Company

AboutPricingContactNevTan

Legal

Data Processing AgreementSubprocessor ListAI & Data Usage PolicySecurity & Compliance

© 2026 NevTan Engage. All rights reserved.

Cookie Policy | Terms and Conditions | Privacy Policy

Skip to main content

Channels & Automation

Email Marketing

Beautiful campaigns that convert

SMS Marketing

Reach customers instantly

Automations

Visual drag-and-drop workflows

By Industry

eCommerce

Boost sales & reduce cart abandonment

SaaS & Apps

Onboard & retain users at scale

B2B Lead Gen

Nurture leads to conversion

Agencies

White-label for your clients

Plans & Pricing

Pricing Plans

Simple, transparent pricing

Learn & Build

Blog

Marketing tips and best practices

Email Marketing Templates

Ready-to-use campaign layouts

Documentation

Guides for every feature in Engage

API

Build custom integrations

About Us

Contact Us

LoginStart Free Trial(No card)

Getting started

  • Introduction
  • Create your account
  • Signing in
  • Your dashboard & navigation

Contacts

  • Contacts & profiles
  • Lists
  • Importing contacts
  • Segments
  • Suppression list

Campaigns

  • Campaigns overview
  • Create & send a campaign
  • Reading a campaign's results

Templates

  • Template library
  • Create with AI
  • The template editor
  • Template approval

Automations

  • Built-in flows
  • Custom flows

Analytics

  • Contact Growth
  • Campaign Reports
  • Automation Reports
  • Metrics glossary

Integrations

  • Webhooks
  • Push Notifications: setup
  • API Keys

Account & billing

  • Your profile
  • Custom sending domains
  • Plans & pricing
  • Billing, usage & invoices
  • Team & roles

Documentation/Integrations

API Keys

Call the Engage API from your own code, without a signed-in session.

An API key lets your own code call Engage directly — send a push, read a segment, add a contact — with no person signed in. You create a key once, store it in your application's configuration, and send it on every request.

⚠️

A key acts on your account with your permissions. Treat it like a password: keep it in environment variables or a secret manager, never in front-end code, a public repository, or a URL.

1. Create a key

1

Open API Keys

From the sidebar, open API Key.

2

Click Create Key

Give it a name that says what will use it, e.g. "Casino site — push sender". You cannot revoke what you cannot identify.

3

Copy the key immediately

The full key is shown once. Engage stores only a hash of it, so it can never be shown again — if you lose it, revoke it and create another.

ℹ️

If you do not see API Key in the sidebar, API access has not been switched on for your company yet — ask your account manager. It is not part of any plan, so upgrading will not add it; an administrator enables it per company.

2. Authenticate

Send the key in the Authorization header on every request. Every key starts with eng_live_.

http
Authorization: Bearer eng_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

The base URL is your Engage API host followed by /api/v1. Set both once and the examples below work as written:

bash
export ENGAGE_API_KEY='eng_live_...'
export ENGAGE_API='https://api.engage.nevtan.com/api/v1'

A quick check that the key works — it touches authentication, the account and permissions in one call:

bash
curl "$ENGAGE_API/push/segments" \
  -H "Authorization: Bearer $ENGAGE_API_KEY"

Every response uses the same envelope, so you can handle success and failure the same way everywhere:

json
{
  "status": "success",
  "message": "",
  "data": [ ... ]
}

3. Create a contact

POST /api/v1/contacts is the simplest complete example — nothing to look up first. It needs the contacts:write permission.

bash
curl -X POST "$ENGAGE_API/contacts" \
  -H "Authorization: Bearer $ENGAGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "apikey@example.com",
    "first_name": "api",
    "last_name": "testing",
    "phone": "+919876543210"
  }'

Every field is optional. Two more are useful: external_id to carry your own identifier for the person, and list_id to drop them straight into a list in the same call.

json
{
  "status": "success",
  "message": "",
  "data": {
    "_id": "000000000000000000000001",
    "email": "apikey@example.com",
    "first_name": "api",
    "last_name": "testing",
    "phone": "+919876543210",
    "subscribed": true,
    "sms_consent": { "status": "subscribed", "source": "manual" },
    "created_at": "2026-01-01T00:00:00Z"
  }
}
⚠️

Check status, not the HTTP code. A duplicate email comes back as HTTP 200 with "status": "failure", not a 409. Code that only tests response.ok will record a rejected contact as created.

Three behaviours worth knowing

  • Duplicate emails are rejected across the whole system, not just your account. If that address exists anywhere, you get Contact with this email already exists. Bulk imports run into this.
  • Sending `phone` opts the contact into SMS. A contact added this way is treated as a manual, consented add, so SMS consent is recorded as subscribed. If you are importing numbers you do not have consent for, leave phone out on create and add it with a PATCH afterwards.
  • The email is not validated. It is stored as given, so not-an-email is accepted.

Read them back with GET /api/v1/contacts, which needs only contacts:read.

4. Send a push

POST /api/v1/push/send needs campaigns:write. Pass a segmentId taken from /push/segments above, or leave it out to reach every live device on the account.

bash
curl -X POST "$ENGAGE_API/push/send" \
  -H "Authorization: Bearer $ENGAGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Your bonus is live",
    "body": "Claim it before midnight.",
    "segmentId": "PASTE_A_SEGMENT_ID_HERE",
    "link": "https://example.com/promo",
    "dryRun": true
  }'
💡

Keep dryRun set to true while you are wiring this up. The message is fully validated and real errors come back, but nothing is delivered to anyone. Remove it when you are ready to send for real.

The response tells you what happened, with no invented certainty about delivery:

json
{
  "status": "success",
  "message": "sent",
  "data": {
    "campaignId": "000000000000000000000002",
    "segmentName": "Active players",
    "contactsInSegment": 4120,
    "reachableContacts": 3874,
    "total": 4192,
    "accepted": 4180,
    "retired": 12,
    "dryRun": false
  }
}
FieldWhat it means
totalDevices the message was addressed to. One person with three devices counts three times.
acceptedDevices the push service took responsibility for. Not a delivery count — what happens after that is invisible to us.
retiredTokens the push service rejected as dead. Engage removes them so the next send is cleaner.
campaignIdThe row this send created in your push history.
ℹ️

accepted is not the same as delivered. A push service returning 200 means it accepted the message for delivery; whether the handset was reachable is something no push platform reports back.

5. Run an email campaign

Four calls, all on campaigns:write except the last. Each returns an id the next one needs, so keep them as you go.

5.1 Put the audience in a list

A campaign sends to lists or segments. Creating a list needs lists:write:

bash
curl -X POST "$ENGAGE_API/lists" \
  -H "Authorization: Bearer $ENGAGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "API test list", "description": "Created from the API" }'

Add someone to it. The contact is created if that email is not on file yet, so this doubles as an import:

bash
curl -X POST "$ENGAGE_API/lists/LIST_ID/contacts" \
  -H "Authorization: Bearer $ENGAGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "apikey@example.com",
    "first_name": "api",
    "last_name": "testing"
  }'

5.2 Create the campaign

channel and type are numbers, not names. For a normal email campaign send channel: 2 (single channel) and type: 1 (email) — and with channel: 2, `date` and `type` are both required or the request is rejected before it reaches the handler.

bash
curl -X POST "$ENGAGE_API/campaign" \
  -H "Authorization: Bearer $ENGAGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "API test campaign",
    "channel": 2,
    "type": 1,
    "date": "2026-01-01T00:00:00Z",
    "subject": "Hello from the API",
    "body": "<h1>Hi {{first_name}}</h1><p>Sent with an API key.</p>",
    "sender": "hello@yourdomain.com",
    "sender_name": "Your Team",
    "reply_to": "hello@yourdomain.com"
  }'
FieldValueMeaning
channel1 / 2Omni-channel / single-channel
type1 / 2 / 3 / 4Email / SMS / IM / Push
dateISO 8601Required when channel is 2
senderan address on a verified domainSending from an unverified domain will fail at send time, not here
⚠️

Send the numbers, not the names. "channel": "single" is rejected with channel Input should be 1 or 2, and "type": "email" with type Input should be 1, 2, 3 or 4. The same numbers come back in every campaign response.

5.3 Send it

list_id and segment_ids are arrays, even for one. template_source chooses where the content comes from: "LIST" for a saved template, "DRAFT" for a builder draft. Omit both and the campaign's own body is used.

bash
curl -X POST "$ENGAGE_API/campaign/CAMPAIGN_ID/send" \
  -H "Authorization: Bearer $ENGAGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "list_id": ["LIST_ID"],
    "batch_size": 100,
    "delay_between_batches": 1.0
  }'
json
{
  "status": "success",
  "message": "",
  "data": {
    "success": true,
    "message": "Campaign sent",
    "total_members": 1,
    "emails_sent": 1,
    "errors": [],
    "started_at": "2026-01-01T00:00:00Z",
    "finished_at": "2026-01-01T00:00:04Z"
  }
}
⚠️

This sends real email the moment it returns. There is no dry-run on this endpoint — unlike /push/send, which has dryRun. Point it at a list containing only your own address the first time.

💡

batch_size (1–1000) and delay_between_batches (0.1–60 seconds) pace the send. The defaults — 100 per batch, one second apart — are a reasonable starting point; slow them down if your domain is newly warmed.

5.4 Read the results

Needs only campaigns:read:

bash
curl "$ENGAGE_API/campaign/CAMPAIGN_ID/stats" \
  -H "Authorization: Bearer $ENGAGE_API_KEY"

Related calls on the same key: GET /campaign lists campaigns, GET /campaign/{id}/recipients/activity shows per-recipient opens and clicks, and POST /campaign/{id}/ab-test/start begins an A/B test.

Account-wide reporting is on the same key: GET /api/v1/analytics/overview, /analytics/performance, /analytics/flows/overview and the rest of /api/v1/analytics all need only analytics:read.

6. Start and stop a flow

Everything the Flows screen does is on the API. GET /api/v1/custom-flows lists your flows with the field that matters here — status, one of draft, live or paused. It needs flows:read.

bash
curl "$ENGAGE_API/custom-flows" \
  -H "Authorization: Bearer $ENGAGE_API_KEY"

Stopping and starting are two separate calls rather than one switch, so a retry cannot turn a flow back on by accident. Both need flows:write.

bash
# stop it — contacts already inside keep their progress
curl -X POST "$ENGAGE_API/custom-flows/FLOW_ID/pause" \
  -H "Authorization: Bearer $ENGAGE_API_KEY"

# start it again — they carry on from where they stopped
curl -X POST "$ENGAGE_API/custom-flows/FLOW_ID/resume" \
  -H "Authorization: Bearer $ENGAGE_API_KEY"
💡

Both are safe to call twice. Pausing a paused flow, or resuming a live one, returns "status": "success" and changes nothing — so a script that cannot see the current state can simply say what it wants.

CallWhat it does
POST /custom-flows/{id}/pauseLive → paused. Nothing new is enrolled and nothing advances.
POST /custom-flows/{id}/resumePaused → live, re-running the same checks the UI runs before activating.
PATCH /custom-flows/{id}/toggleFlips whichever state it finds. What the UI's switch calls.
PATCH /custom-flows/{id}A body of only {"status": "paused"} or {"status": "live"} makes the same state change, for when you are already sending a PATCH. Send anything else alongside it and the request is refused.
POST /custom-flows/{id}/duplicateCopies the flow to a new draft you can edit.

A resume can be refused, and the message says why: a flow that no longer validates lists what is missing, and an account out of email credits is told to top up. Nothing is half-started.

⚠️

A flow's content is locked once it has gone live — its steps cannot be rewritten while contacts are partway through them. Pausing does not unlock it. To change a live flow, duplicate it, edit the draft copy, and publish that.

What a key can and cannot do

A key is issued with everything the person who created it can do, and that now covers essentially the whole product: contacts, lists, segments, campaigns, flows, templates, push, landing pages and signup forms, ecommerce, events and analytics. If you can do it on the screen, you can do it with the key. It is never wider than its creator: if your own permissions are reduced later, every key you made narrows with them.

Four things are deliberately withheld from every key, however wide its creator's access:

  • Adding or changing team members
  • Billing
  • Changing account settings
  • Creating or listing other API keys

These are the permissions that would let a stolen key entrench itself — invite an accomplice, or mint fresh keys to survive the one you revoke. Nothing an integration legitimately does needs them.

Two smaller things stay out for the same reason: uploading or removing the account's Firebase credentials, and reading invoices. Sending push works on a key; replacing the credentials it sends with does not.

Rate limits

Each key may make 120 requests per minute. The limit is per key, not per server, so scaling your integration across more workers does not buy more requests.

Going over returns HTTP 429 with a message telling you how long to wait. Back off and retry; do not spin.

json
{
  "message": "Too many requests. Try again in 34 seconds.",
  "success": false
}

Revoking a key

Open API Key, find the key in the table and click the revoke icon in the Actions column. It stops working immediately, and anything using it starts failing on the next call. The row stays in the table, greyed out and marked Revoked.

💡

The record of the key is kept rather than erased, so you can still see what it was called and when it was last used. "Which key could send in March" is a question that outlives the key itself.

Revoke a key the moment you suspect it has leaked, and whenever the integration that used it is retired. Keys do not expire on their own.

Errors

StatusMeaningWhat to do
401The key is unknown, revoked or expired — or the account it belongs to is deactivatedCheck the key was copied in full, then create a new one
429Rate limit exceededWait the number of seconds in the message, then retry
200 with "status": "failure"Everything else, including a key that lacks the permission for that endpointRead message. For a permission failure it reads *You don't have permission to perform this action*
⚠️

There is no 403. A permission failure comes back as HTTP 200 with "status": "failure", the same envelope as any other business failure. Code that branches on response.status === 403 will never see it, and code that only checks response.ok will read a refusal as a success.

ℹ️

A 401 is deliberately the same answer for an unknown key, a revoked key and an expired one. Distinguishing them would let anyone with a list of guesses learn which keys exist.

Good practice

  • One key per integration. Then revoking a compromised key takes down one thing, not everything.
  • Keep it server-side. A key in browser or mobile code is a key you have published.
  • Rotate deliberately. Create the new key, deploy it, confirm traffic has moved, then revoke the old one — in that order, so nothing goes dark.
  • Watch Last used. A key that has not been used in months is a key nobody will miss when you revoke it.

Previous

Push Notifications: setup

Next

Your profile

Still need a hand?

Our support team usually replies within one business day.

Contact support

On this page

  • 1. Create a key
  • 2. Authenticate
  • 3. Create a contact
  • 4. Send a push
  • 5. Run an email campaign
  • 6. Start and stop a flow
  • What a key can and cannot do
  • Rate limits
  • Revoking a key
  • Errors
  • Good practice