Supercommerce API Docs
Admin API

Klaviyo Marketing Module — Admin

HTTP surface for the Klaviyo marketing-automation integration — connect/disconnect the Klaviyo account, inspect each outbound sync surface (profiles, events, product catalog,…

HTTP surface for the Klaviyo marketing-automation integration — connect/disconnect the Klaviyo account, inspect each outbound sync surface (profiles, events, product catalog, categories, coupons), triage failed rows, and force resyncs/bootstraps. Outbound sync runs asynchronously via BullMQ; these endpoints are the operator's control panel over that pipeline. Inbound Klaviyo webhooks are documented separately (see ../webhooks/klaviyo.md).

Source: api-modules/marketing-klaviyo/src/controllers/ (admin-klaviyo*.controller.ts).

Optional plugin. Registered via MarketingKlaviyoModule.forRoot() in apps/api/src/app.module.ts. Removing that line disables the plugin entirely — listeners stop firing, BullMQ processors never register, these admin endpoints disappear from the OpenAPI spec, and the inbound webhook route 404s. No core table references this module's data. There are additional runtime kill switches (settings, see below) layered under the module-level switch.


Conventions

Authentication

All endpoints require a Better-Auth admin session and a role granting the matching klaviyo:* permission. There are exactly two actions:

ActionGates
klaviyo: viewevery GET (status + all failed/list reads)
klaviyo: manageevery mutating POST/DELETE (connect, disconnect, resync, bootstrap)

HTTP status codes

Every mutating POST overrides the NestJS default 201 to 200 (@HttpCode(200)). DELETE returns 200.

Response envelope

Successful responses are wrapped by ResponseInterceptor:

{
  "data": <payload>,
  "message": "Success",
  "statusCode": 200,
  "metadata": { /* on list endpoints */ }
}

Paginated lists carry metadata: { total, limit, offset, hasMore } (the platform-standard ApiWrappedPaginatedResponse shape).

Error envelope

statusCodeerrorCode examples
400BAD_REQUEST, VALIDATION_ERROR
401UNAUTHORIZED
403FORBIDDEN
404NOT_FOUND (events resync, malformed id only)
500INTERNAL_SERVER_ERROR, DATABASE_ERROR

Runtime kill switches (settings, klaviyo group)

Independent of the module-level on/off. Master sync_enabled plus per-surface profile_sync_enabled, event_sync_enabled, catalog_sync_enabled, coupon_sync_enabled, and webhook_ingest_enabled. The connection status endpoint reports the resolved per-surface state.

Async semantics

  • resync/:id endpoints are fire-and-forget enqueues — they return { enqueued: true } once the row is in the outbox; a drain processor picks it up on the next tick. They do not wait for the push to Klaviyo.
  • bootstrap endpoints enqueue every row of the relevant kind and return { enqueued: <count> }.

Connection lifecycle

Base path: /admin/klaviyo.

GET /admin/klaviyo/status — Connection + configuration status

Required permission: klaviyo: view. Masked summary only — the raw API key is never returned.

Response 200

type KlaviyoConnectionStatusResponse = {
  connected: boolean;
  accountId: string | null;
  connectedAt: string | null;          // ISO
  apiKeyMasked: string | null;
  syncEnabled: boolean;                // master sync switch
  perSurfaceEnabled: {
    profile: boolean;
    event: boolean;
    catalog: boolean;
    coupon: boolean;
    webhookIngest: boolean;
  };
  configuration: {
    feed: "configured" | "missing";
    missingKeys: string[];
    revision: string;                  // Klaviyo API revision (e.g. "2024-10-15")
  };
};

POST /admin/klaviyo/connect — Connect (verify + persist API key)

Required permission: klaviyo: manage. Live-probes the key against Klaviyo's /accounts endpoint and persists it on success. The key is never echoed back.

Body

{ "apiKey": "pk_live_xxxxxxxxxxxxxxxxxxxx" }
FieldTypeConstraints
apiKeystringTrimmed, 20..200 chars (Klaviyo private API keys are ≥ 20 chars)

Response 200

type KlaviyoConnectResponse = {
  connected: true;
  accountId: string;
  connectedAt: string;                 // ISO
};

Errors

StatusCodeWhen
400VALIDATION_ERRORKey fails zod (too short/long)
401/4xxupstreamKlaviyo rejects the key during the live /accounts probe

DELETE /admin/klaviyo/connect — Disconnect

Required permission: klaviyo: manage. Drops the persisted credential row and busts the in-memory SDK session.

Response 200{ "disconnected": true }.


Profile sync

Base path: /admin/klaviyo/profiles. Mirrors customers into Klaviyo profiles.

Shared item shape — KlaviyoProfileStateItem:

type KlaviyoProfileSyncStatus     = "pending" | "synced" | "failed" | "skipped";
type KlaviyoSubscriptionStatus    = "subscribed" | "unsubscribed" | "suppressed" | "never_subscribed";

type KlaviyoProfileStateItem = {
  id: string;
  customerId: string | null;
  email: string | null;
  klaviyoProfileId: string | null;
  status: KlaviyoProfileSyncStatus;
  subscriptionStatus: KlaviyoSubscriptionStatus;
  lastError: string | null;
  attempts: number;
  lastSyncedAt: string | null;         // ISO
  updatedAt: string;                   // ISO
};

GET /admin/klaviyo/profiles — All profile mirror rows

Required permission: klaviyo: view. Lists all profile rows (not just failed).

Query (shared by every list endpoint in this module)

NameTypeDefaultConstraints
pageint1>= 1
limitint501..200

Response 200 — paginated KlaviyoProfileStateItem[].

GET /admin/klaviyo/profiles/failed — Failed profile rows

Required permission: klaviyo: view. Same query/response, filtered to status = failed.

POST /admin/klaviyo/profiles/resync/:customerId — Re-push one customer

Required permission: klaviyo: manage. Enqueues an upsert for customerId. Response 200{ "enqueued": true }.

POST /admin/klaviyo/profiles/bootstrap — Re-push every customer

Required permission: klaviyo: manage. Use after first connect. Response 200{ "enqueued": <count> }.


Event sync

Base path: /admin/klaviyo/events. Streams ecommerce metrics (Placed Order, Started Checkout, Added to Cart, etc.) to Klaviyo via an outbox.

Item shape — KlaviyoEventOutboxItem:

type KlaviyoEventSyncStatus = "pending" | "sent" | "failed" | "dropped";

type KlaviyoEventOutboxItem = {
  id: string;
  metricName: string;                  // e.g. "Placed Order"
  idempotencyKey: string;
  customerId: string | null;
  email: string | null;
  status: KlaviyoEventSyncStatus;
  occurredAt: string;                  // ISO
  enqueuedAt: string;                  // ISO
  processedAt: string | null;          // ISO
  attempts: number;
  lastError: string | null;
  valueCents: number | null;           // integer subunits
  valueCurrency: string | null;
};

GET /admin/klaviyo/events/failed — Failed event rows

Required permission: klaviyo: view. Paginated (shared page/limit), newest first.

POST /admin/klaviyo/events/resync/:eventId — Requeue a failed event

Required permission: klaviyo: manage. Event payloads are immutable once enqueued, so this just resets the row to status=pending, attempts=0, and clears processedAt; the drain re-sends on the next tick.

Path paramseventId: outbox row id.

Response 200{ "enqueued": true }.

Errors

StatusCodeWhen
404NOT_FOUNDOnly when eventId is malformed (fails the ^[0-9a-f-]{8,}$ id format check). A well-formed id that isn't currently failed is accepted — operators sometimes re-deliver already-sent rows.

Catalog sync

Base path: /admin/klaviyo/catalog. Pushes product variants to the Klaviyo catalog.

Item shape — KlaviyoCatalogStateItem:

type KlaviyoCatalogSyncStatus = "pending" | "submitted" | "synced" | "failed" | "deleted" | "skipped";

type KlaviyoCatalogStateItem = {
  variantId: string;
  klaviyoCatalogItemId: string | null;
  status: KlaviyoCatalogSyncStatus;
  lastError: string | null;
  attempts: number;
  lastPushedAt: string | null;         // ISO
  updatedAt: string;                   // ISO
};

GET /admin/klaviyo/catalog/failed — Failed variant syncs

Required permission: klaviyo: view. Paginated (shared page/limit).

POST /admin/klaviyo/catalog/resync/:variantId — Re-push one variant

Required permission: klaviyo: manage. Response 200{ "enqueued": true }.

POST /admin/klaviyo/catalog/bootstrap — Re-push every variant

Required permission: klaviyo: manage. Use after first connect or after re-mapping rules change. Response 200{ "enqueued": <count> }.


Category sync

Base path: /admin/klaviyo/categories. Pushes categories to Klaviyo.

Item shape — KlaviyoCategoryStateItem:

type KlaviyoCategorySyncStatus = "pending" | "synced" | "failed" | "deleted" | "skipped";

type KlaviyoCategoryStateItem = {
  categoryId: string;
  klaviyoCategoryId: string | null;
  status: KlaviyoCategorySyncStatus;
  lastError: string | null;
  attempts: number;
  lastPushedAt: string | null;         // ISO
  updatedAt: string;                   // ISO
};

GET /admin/klaviyo/categories/failed — Failed category syncs

Required permission: klaviyo: view. Paginated (shared page/limit), newest first.

POST /admin/klaviyo/categories/resync/:categoryId — Re-push one category

Required permission: klaviyo: manage. Response 200{ "enqueued": true }.

POST /admin/klaviyo/categories/bootstrap — Re-push every category

Required permission: klaviyo: manage. Response 200{ "enqueued": <count> }.


Coupon sync

Base path: /admin/klaviyo/coupons. Pushes discounts to Klaviyo as coupons. No bootstrap — operators typically have only tens of discounts, so per-discount resync suffices.

Item shape — KlaviyoCouponStateItem:

type KlaviyoCouponSyncStatus = "pending" | "synced" | "failed" | "deleted" | "skipped";

type KlaviyoCouponStateItem = {
  discountId: string;
  klaviyoCouponId: string | null;
  klaviyoCouponCodeId: string | null;
  lastPushedCode: string | null;
  status: KlaviyoCouponSyncStatus;
  lastError: string | null;
  attempts: number;
  lastPushedAt: string | null;         // ISO
  updatedAt: string;                   // ISO
};

GET /admin/klaviyo/coupons/failed — Failed coupon syncs

Required permission: klaviyo: view. Paginated (shared page/limit), newest first.

POST /admin/klaviyo/coupons/resync/:discountId — Re-push one discount

Required permission: klaviyo: manage. Response 200{ "enqueued": true }.


  • admin-rbac — gates every endpoint via klaviyo:* permissions (view / manage). See admin-rbac.md.
  • settings — the klaviyo settings group holds the API key, webhook secret, revision, and the master + per-surface kill switches.
  • integration — the plugin registers a no-op token refresher (getFreshAccessToken("klaviyo")); Klaviyo private API keys never expire.
  • customer / catalog / discount / order / cart — domain events from these modules drive the profile, catalog, category, coupon, and event sync surfaces.
  • Inbound webhooks — Klaviyo → us, mirroring consent state. See ../webhooks/klaviyo.md.

On this page