Server-side integration guide

Connect EngageApp to your backend with a secret API key — sync contacts, track events, and enroll contacts into campaigns.

This guide walks you through integrating EngageApp with your backend. With a single secret API key your server can keep contacts in sync, stream the behavioral events that drive automation, and enroll people into campaigns — all over a small, JSON HTTP API.

Everything here uses the public API served from https://api.engageapp.xyz. For the terse endpoint list, see the Public API reference.

What you can integrate

From your backend you can:

  • Sync contacts — create or update the people EngageApp can message, keyed by your own user IDs.
  • Track events — send product and behavioral events that segment contacts and trigger campaigns.
  • Enroll contacts — drop a contact into a specific campaign on demand.

Creating campaigns, editing templates, and configuring channels are done in the dashboard — not from your customer-facing backend. A typical integration is just two or three calls:

1. POST /api/v1/public/contacts   → tell EngageApp who the person is
2. POST /api/v1/public/events     → tell EngageApp what they did
3. POST /api/v1/public/enroll     → (optional) put them in a campaign

In most setups you only need steps 1 and 2: events do the rest by triggering event-driven campaigns automatically.

Before you start: create a secret key

Server-side calls authenticate with a secret key (df_…). Create one under Settings → API keys and copy it immediately — the full value is shown only once. Store it in your secrets manager, never in client code. See API keys for details on rotating and revoking.

Authentication

Send your secret key in the X-API-Key header on every request:

X-API-Key: df_your_secret_key
Content-Type: application/json

A missing, invalid, or revoked key returns 401 Unauthorized. Your key is scoped to exactly one workspace — every contact, event, and enrollment you create is automatically isolated to it, so there is no workspace parameter to pass.

Do not confuse this with the publishable key (pk_…) used by the client SDKs: publishable keys are rejected on every server-side endpoint, and secret keys are rejected on the SDK endpoints.

The join key: external_id

Every contact endpoint accepts an external_id — your own primary key for the user (e.g. user_8123). Using it consistently is the most important thing you can do for a clean integration: it lets you address contacts by your ID without first storing EngageApp's internal contact ID.

  • POST /public/contacts upserts by external_id — create-or-update in one call, safe to send on every profile change.
  • POST /public/events and POST /public/enroll both resolve the contact by external_id.

Every endpoint also accepts EngageApp's contact_id as an alternative, if you'd rather store that.

Step 1 — Sync contacts

POST /api/v1/public/contacts batch upserts contacts (up to 100 per request) and can register push-notification devices. Entries missing both external_id and contact_id are skipped.

curl -X POST https://api.engageapp.xyz/api/v1/public/contacts \
  -H "X-API-Key: df_your_secret_key" \
  -H "Content-Type: application/json" \
  -d '{
    "contacts": [
      {
        "external_id": "user_8123",
        "email": "[email protected]",
        "phone": "+2348012345678",
        "first_name": "Ada",
        "last_name": "Lovelace"
      }
    ]
  }'

Fields: external_id (upserts the contact) or contact_id; optional email, phone (E.164 recommended), first_name, last_name, and a devices array (each with platformios, android, or web — and a push_token).

Response:

{ "updated": 1 }

updated is the count of entries successfully created or updated.

Step 2 — Track events

POST /api/v1/public/events ingests a batch of behavioral events (up to 100 per request). Events are stored per contact, feed segmentation and conversion tracking, and trigger event-driven campaigns. event_name is required; the contact is resolved by external_id (or contact_id, email, phone).

curl -X POST https://api.engageapp.xyz/api/v1/public/events \
  -H "X-API-Key: df_your_secret_key" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      {
        "external_id": "user_8123",
        "event_name": "purchase_completed",
        "properties": { "order_id": "ord_5567", "amount": 149.99, "currency": "USD" },
        "occurred_at": "2026-07-23T09:41:00Z"
      }
    ]
  }'

Fields: event_name (required), a contact reference, optional properties (any JSON — usable in segment/trigger filters and template variables), and an optional ISO-8601 occurred_at (defaults to ingest time).

Response:

{ "ingested": 1 }

Backfilling historical events

To load past events without retroactively firing automation, use the dashboard's historical import (or POST /api/v1/events/import, up to 5000 per request). Imported events populate segments and conversion tracking but are skipped by trigger evaluation, so live campaigns won't re-fire on old data.

Step 3 — Enroll contacts (optional)

POST /api/v1/public/enroll explicitly enrolls one contact into one campaign. Use it when your backend should decide enrollment directly, rather than relying on an event trigger. The campaign must be active.

curl -X POST https://api.engageapp.xyz/api/v1/public/enroll \
  -H "X-API-Key: df_your_secret_key" \
  -H "Content-Type: application/json" \
  -d '{ "campaign_id": "camp_123", "external_id": "user_8123" }'

Provide campaign_id plus a contact reference (external_id or contact_id). The response is the enrollment record:

{
  "id": "enr_456",
  "campaign_id": "camp_123",
  "contact_id": "cont_789",
  "status": "active",
  "entered_at": "2026-07-23T10:05:00Z"
}

If workspace billing is unresolved, the enrollment is still accepted but dispatches are paused — the response includes "billing_action_required": true. Errors: 400 (missing campaign_id or contact reference), 404 (no contact matches the external_id), 409 (campaign not active).

Trigger campaigns automatically from events

The recommended pattern is event-driven — you stream events and campaigns enroll matching contacts on their own, so you never call /public/enroll:

  1. In the dashboard, create a campaign with an event trigger (e.g. on cart_abandoned), optionally filtered by event properties such as cart_value greater than 50.
  2. From your backend, POST /public/events with that event_name whenever it happens.
  3. EngageApp matches the event against active event-triggered campaigns and enrolls the contact. The triggering event is snapshotted onto the enrollment, so its properties are available as template variables.

Use explicit enrollment for cases that aren't tied to a single event — for example, a nightly job that adds every trial user to an onboarding drip.

Batching & rate limits

Server-side endpoints are rate limited per workspace at 30 requests/second (burst 50). Exceeding it returns 429 Too Many Requests with a Retry-After header:

{ "error": "rate limit exceeded", "status": 429, "retry_after": 1 }

Because contacts and events are batch endpoints (100 items per request), you rarely approach this limit — prefer one request of 100 items over 100 requests of one item. Honor Retry-After and back off on 429.

Error handling

Errors share a consistent JSON envelope:

{
  "error": "campaign_id is required",
  "status": 400,
  "path": "/api/v1/public/enroll",
  "request_id": "..."
}
StatusMeaning
400Malformed body or missing required field.
401Missing, invalid, or revoked API key.
404Referenced contact or campaign not found.
409Conflicting state (e.g. enrolling into a non-active campaign).
429Rate limit exceeded — honor Retry-After.
5xxServer error — retry with backoff.

Retry 429 and 5xx with exponential backoff; fix 4xx requests rather than retrying them. Upserts keyed by a stable external_id (and events keyed by a stable order_id in properties) are naturally idempotent, so retries after a network timeout are safe. Include the request_id when reporting an issue.

A minimal Node integration

const BASE = "https://api.engageapp.xyz/api/v1";
const KEY = process.env.ENGAGE_API_KEY;

async function engage(path, body) {
  const res = await fetch(BASE + path, {
    method: "POST",
    headers: { "X-API-Key": KEY, "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  if (res.status === 429) {
    const retry = Number(res.headers.get("Retry-After") ?? 1);
    await new Promise((r) => setTimeout(r, retry * 1000));
    return engage(path, body); // retry once
  }
  if (!res.ok) throw new Error("Engage " + path + " failed: " + res.status);
  return res.json();
}

// On signup:
await engage("/public/contacts", {
  contacts: [{ external_id: "user_8123", email: "[email protected]", first_name: "Ada" }],
});

// On purchase (may auto-trigger a campaign):
await engage("/public/events", {
  events: [{ external_id: "user_8123", event_name: "purchase_completed", properties: { amount: 149.99 } }],
});

Going to production

  • Use a separate secret key per environment (staging vs. production).
  • Store keys in a secrets manager; keep anything client-side on a publishable key.
  • Key contacts and events by a stable external_id from day one.
  • Batch up to 100 items per request instead of one call per item.
  • Handle 429 and retry 5xx with backoff; make retries idempotent.
  • Backfill historical data via the import path so it doesn't re-trigger campaigns.
  • Keep your credit balance funded so dispatches aren't paused.
  • Have a key rotation plan: create new → deploy → revoke old.

Next steps