Subscription CRUD routes require Authorization: Bearer <project-api-key>. An unpinned project key may act for any user in that project. A user-pinned key is restricted to its pinned user.

Discover triggers with the SDK

Trigger definitions come from the local catalog and require no HTTP request:

ts
import { Eyeball } from "@eyeball/sdk";

const eyeball = new Eyeball({
  apiKey: process.env.EYEBALL_API_KEY!,
  baseUrl: process.env.EYEBALL_EXECUTOR_URL!,
});

const triggers = await eyeball.triggers.list({
  capability: "email",
  deliveryMode: "polling",
});

The result is an array of canonical TriggerDefinition values with name, toolkit, capability, description, payloadSchema, annotations, and version.

Create a subscription

POST /v1/subscriptions

JSON
{
  "trigger": "slack.message_received",
  "userId": "user_123",
  "connectionId": "conn_slack_123",
  "webhookEndpointIds": ["whe_123"],
  "filters": {
    "conversationId": "C012345"
  }
}

The executor validates:

  • the canonical trigger and provider implementation;
  • the user-scoped connection, auth class, expiry, and effective scopes;
  • distinct project-owned webhook endpoints subscribed to the exact trigger or trigger.*;
  • trigger-specific portable filters; and
  • pollIntervalSeconds for polling triggers.

A 201 push response includes the public subscription plus ingestUrl:

JSON
{
  "subscriptionId": "trgsub_123",
  "projectId": "proj_123",
  "userId": "user_123",
  "trigger": "slack.message_received",
  "connectionId": "conn_slack_123",
  "webhookEndpointIds": ["whe_123"],
  "filters": { "conversationId": "C012345" },
  "status": "active",
  "createdAt": "2026-07-18T09:00:00.000Z",
  "updatedAt": "2026-07-18T09:00:00.000Z",
  "ingestUrl": "https://executor.example.com/v1/ingest/trgsub_123/trgsec_..."
}

ingestUrl appears only in the create response. Polling subscriptions omit it and include the selected pollIntervalSeconds. Treat the entire URL as a credential: provider callback products often cannot send a custom authentication header, so the secret must live in the path and may otherwise appear in proxy access logs.

The SDK equivalent is eyeball.subscriptions.create(options).

List subscriptions

GET /v1/subscriptions?userId=user_123&limit=100&cursor=...

The response is { subscriptions, nextCursor? }. limit must be from 1 through 100. List responses never contain an ingest secret, ingest URL, or provider cursor.

For a pinned key, the executor forces the pinned user filter. A conflicting userId query or X-Eyeball-User-Id header returns 403 auth_insufficient_scope. An unpinned project key may omit userId to list the project.

The SDK equivalent is eyeball.subscriptions.list(options).

List recent trigger events

GET /v1/trigger-events?limit=100&cursor=...&subscriptionId=trgsub_123&trigger=slack.message_received

This is project-wide operational history and therefore requires an unpinned project API key. Every user-pinned key receives 403 auth_insufficient_scope; the route does not accept a userId query or header override.

Query parameterRequiredMeaning
limitNoInteger from 1 through 100; defaults to 100.
cursorNoOpaque cursor from the preceding response.
subscriptionIdNoExact valid trgsub_* subscription ID.
triggerNoExact canonical dotted trigger name.

Rows are newest receivedAt first. The response envelope is { triggerEvents, nextCursor? }:

JSON
{
  "triggerEvents": [
    {
      "arrivalId": "trgevt_01JZ...",
      "eventId": "evt_trigger_8f0...",
      "subscriptionId": "trgsub_123",
      "trigger": "slack.message_received",
      "deliveryMode": "push",
      "receivedAt": "2026-07-21T14:00:00.000Z",
      "occurredAt": "2026-07-21T13:59:58.000Z",
      "dedupStatus": "accepted",
      "deliveryStatus": "succeeded",
      "requestedWebhookEndpointIds": ["whe_123"],
      "deliveryTargets": [
        {
          "endpointId": "whe_123",
          "deliveryId": "whd_123",
          "status": "succeeded"
        }
      ],
      "expiresAt": "2026-07-28T14:00:00.000Z"
    }
  ],
  "nextCursor": "eyJ..."
}

receivedAt is the executor's normalized-arrival time; occurredAt is the validated provider occurrence time. The first arrival has dedupStatus: "accepted". A provider retry inside the dedup window creates a separate row with its own arrivalId, the accepted arrival's stable eventId, dedupStatus: "duplicate", deliveryStatus: "not_enqueued", and no actual delivery targets.

requestedWebhookEndpointIds snapshots the subscription's configured endpoint IDs at admission time. deliveryTargets contains only endpoints that the webhook worker actually materialized after checking endpoint existence, activity, and event subscriptions. Aggregate deliveryStatus is one of not_enqueued, admission_failed, selecting, no_targets, pending, delivering, succeeded, failed, or partial; each materialized target uses pending, delivering, succeeded, or failed.

History expires seven days after receivedAt, independently of the 24-hour dedup claim. Expired rows are hidden immediately and physically removed in bounded cleanup batches. If a cursor's anchor has already been swept, the cursor returns 422 invalid_input; restart pagination from the first page. Deleting a subscription or endpoint does not delete its existing history before expiresAt.

The store and response deliberately exclude provider event IDs, payload bodies, canonical webhook data, user/provider message content, filters, polling cursors, ingest URLs and secrets, endpoint URLs, signing secrets, credentials, headers, attempt bodies, and transport error text. History cannot reconstruct the signed webhook body.

ts
import { Eyeball } from "@eyeball/sdk";

const eyeball = new Eyeball({
  apiKey: process.env.EYEBALL_API_KEY!,
  baseUrl: process.env.EYEBALL_EXECUTOR_URL!,
});

let page = await eyeball.triggerEvents.list({
  trigger: "slack.message_received",
  limit: 20,
});

for (const event of page.triggerEvents) {
  console.log(event.eventId, event.deliveryStatus);
  for (const target of event.deliveryTargets) {
    console.log(target.endpointId, target.status);
  }
}

if (page.nextCursor) {
  page = await eyeball.triggerEvents.list({
    trigger: "slack.message_received",
    limit: 20,
    cursor: page.nextCursor,
  });
}

Get a subscription

GET /v1/subscriptions/:subscriptionId

Returns the project-owned public subscription or 404. A pinned key may read only its pinned user's subscription. The response never contains the ingest secret, ingest URL, or provider cursor.

The SDK equivalent is eyeball.subscriptions.get(subscriptionId).

Rotate a push ingest secret

POST /v1/subscriptions/:subscriptionId/rotate-secret

The response contains { subscriptionId, ingestUrl, rotatedAt }. The old URL stops authenticating immediately, and the replacement URL appears only in this response. Update the provider callback before discarding it. Polling subscriptions return 422 because they have no ingest secret. A pinned key may rotate only its pinned user's subscription.

The SDK equivalent is eyeball.subscriptions.rotateSecret(subscriptionId).

Delete a subscription

DELETE /v1/subscriptions/:subscriptionId

Returns 204 when deleted or 404 when the subscription is absent from the authenticated project. A pinned key cannot delete another user's subscription. Deletion removes cursor and dedup state from the configured in-memory or Postgres store and prevents future ingestion. Existing redacted event-history rows remain visible to an unpinned project key until their individual seven-day expiry.

The SDK equivalent is eyeball.subscriptions.delete(subscriptionId).

Provider push ingestion

POST /v1/ingest/:subscriptionId/:secret

This route does not accept an Eyeball API key. The secret embedded in the create-time ingest URL authenticates the provider callback. A missing subscription, inactive subscription, wrong secret, or non-push subscription returns the same 404 response.

Disable or redact request-target logging for /v1/ingest/* at the load balancer, reverse proxy, APM agent, and executor host. Rotate the secret after any suspected log or URL disclosure. The path credential is an explicit compatibility tradeoff for provider callback systems that do not support custom authentication headers.

Slack URL verification:

JSON
{
  "type": "url_verification",
  "challenge": "provider-challenge"
}

returns 200 with:

JSON
{ "challenge": "provider-challenge" }

A valid Slack event_callback returns 202:

JSON
{
  "kind": "accepted",
  "accepted": 1,
  "duplicates": 0
}

The response acknowledges ingestion into the asynchronous signed webhook pipeline. Inspect project trigger-event history through GET /v1/trigger-events for arrival and dedup status, then inspect GET /v1/webhooks/:endpointId/deliveries for full per-endpoint attempt metadata.

Errors

StatusTypical meaning
401Missing or invalid API key on subscription CRUD or event-history reads.
403Pinned key attempted to act for a different user, or any pinned key attempted to read project-wide event history.
404Subscription is missing, cross-project, inactive for ingestion, or has a wrong ingest secret.
422Invalid trigger, connection, scope, endpoint, filter, polling cadence, or provider callback body.

The project Triggers screen in the dashboard exposes subscription creation with catalog-backed trigger selection and mode-specific polling cadence, the reveal-once push ingest URL on create and rotation, confirmed rotation and deletion, metadata-only subscription listings, and a project-level Recent events tab. It uses the selected project's executor path in both demo and cloud mode.

Read Triggers for payloads and dedup semantics, and Webhooks API for endpoint management and delivery logs.