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
Open API Keys
From the sidebar, open API Key.
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.
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, your plan does not include it. Ask your account manager to enable API access.
2. Authenticate
Send the key in the Authorization header on every request. Every key starts with eng_live_.
Authorization: Bearer eng_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxThe base URL is your Engage API host followed by /api/v1. Set both once and the examples below work as written:
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:
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:
{
"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.
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.
{
"status": "success",
"message": "",
"data": {
"_id": "000000000000000000000001",
"email": "apikey@example.com",
"first_name": "api",
"last_name": "testing",
"phone": "+919876543210",
"subscribed": true,
"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
phoneout on create and add it with aPATCHafterwards. - The email is not validated. It is stored as given, so
not-an-emailis 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.
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:
{
"status": "success",
"message": "sent",
"data": {
"campaignId": "000000000000000000000002",
"segmentName": "Active players",
"contactsInSegment": 4120,
"reachableContacts": 3874,
"total": 4192,
"accepted": 4180,
"retired": 12,
"dryRun": false
}
}| Field | What it means |
|---|---|
total | Devices the message was addressed to. One person with three devices counts three times. |
accepted | Devices the push service took responsibility for. Not a delivery count — what happens after that is invisible to us. |
retired | Tokens the push service rejected as dead. Engage removes them so the next send is cleaner. |
campaignId | The 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:
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:
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.
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"
}'| Field | Value | Meaning |
|---|---|---|
channel | 1 / 2 | Omni-channel / single-channel |
type | 1 / 2 / 3 / 4 | Email / SMS / IM / Push |
date | ISO 8601 | Required when channel is 2 |
sender | an address on a verified domain | Sending from an unverified domain will fail at send time, not here |
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.
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
}'{
"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:
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.
Analytics endpoints under /api/v1/analytics do not accept an API key yet — they still require a signed-in session. Per-campaign numbers from /campaign/{id}/stats do work, so most reporting is reachable without them.
What a key can and cannot do
A key is issued with everything the person who created it can do — sending campaigns, reading and writing contacts, segments and templates, reading analytics. 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.
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.
{
"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 delete icon. It stops working immediately, and anything using it starts failing on the next call.
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
| Status | Meaning | What to do |
|---|---|---|
| 401 | The key is unknown, revoked, or expired — or the account it belongs to is deactivated | Check the key was copied in full, then create a new one |
| 403 | The key is valid but lacks the permission for that endpoint, or API keys are not enabled for the account | Check the endpoint against the list above; a key never holds team, billing, settings or key-management permissions |
| 429 | Rate limit exceeded | Wait the number of seconds in the message, then retry |
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.
