Hogsend
API Reference

Ingestion & Webhooks

Event ingestion endpoint, Resend webhook handler, generic webhook sources, and health check.

Health

GET /v1/health

Returns service health status. Used by Railway health checks and monitoring.

Response 200

{
  "status": "healthy",
  "uptime": 12345.678,
  "timestamp": "2025-01-15T10:30:00.000Z",
  "version": "0.0.1",
  "components": {
    "database": { "status": "up", "latencyMs": 1 },
    "redis": { "status": "up", "latencyMs": 1 }
  }
}
FieldTypeDescription
status"healthy" | "degraded"Current service status. degraded if any component is down
uptimenumberProcess uptime in seconds
timestampstringISO 8601 timestamp
versionstringAPI version
componentsobjectPer-component health status
components.databaseobjectDatabase health
components.database.status"up" | "down"Database connectivity status
components.database.latencyMsnumberDatabase ping latency in milliseconds
components.redisobjectRedis health
components.redis.status"up" | "down"Redis connectivity status
components.redis.latencyMsnumberRedis ping latency in milliseconds
curl http://localhost:3002/v1/health

Ingestion

POST /v1/ingest

Receives events from direct API calls. Stores the event in userEvents, pushes it to Hatchet for journey routing, processes exit conditions on active journeys, and upserts the contact record.

Request Body

{
  "event": "user:signed_up",
  "userId": "user_abc123",
  "userEmail": "[email protected]",
  "properties": {
    "plan": "pro",
    "source": "website"
  },
  "timestamp": "2025-01-15T10:30:00.000Z",
  "idempotencyKey": "evt_abc123_signup_2025"
}
FieldTypeRequiredDescription
eventstringYesEvent name (min 1 char)
userIdstringYesExternal user identifier (min 1 char)
userEmailstringNoUser email address (validated format)
propertiesRecord<string, unknown>NoArbitrary event properties
timestampstringNoISO 8601 datetime (defaults to now)
idempotencyKeystringNoUnique key for deduplication. If a duplicate key is sent, the event is silently dropped and stored: false is returned

Response 202

{
  "stored": true,
  "exits": [
    {
      "journeyId": "onboarding-welcome",
      "stateId": "550e8400-e29b-41d4-a716-446655440000",
      "exited": false
    }
  ]
}
FieldTypeDescription
storedbooleanWhether the event was persisted. Returns false when a duplicate idempotencyKey is detected
exitsExitResult[]Exit condition evaluation results for active journeys
exits[].journeyIdstringJourney that was checked
exits[].stateIdstringJourney state ID
exits[].exitedbooleanWhether the user exited this journey
curl -X POST http://localhost:3002/v1/ingest \
  -H "Content-Type: application/json" \
  -d '{
    "event": "user:signed_up",
    "userId": "user_abc123",
    "userEmail": "[email protected]",
    "properties": { "plan": "pro" }
  }'

Webhooks

POST /v1/webhooks/resend

Receives webhook events from Resend (email delivery status updates). Handles signature verification internally via the EmailService.

Request Body -- Raw Resend webhook payload (Record<string, unknown>).

Headers -- Resend includes signature headers for verification against RESEND_WEBHOOK_SECRET.

Response 200

{ "ok": true }

Response 401

{ "error": "Email service not configured" }

or

{ "error": "Webhook verification failed" }

This endpoint processes email events like email.delivered, email.bounced, email.complained, etc. The EmailService tracks bounces and updates suppression status when the bounce threshold (default: 3) is reached.

POST /v1/webhooks/{sourceId}

Generic webhook ingestion endpoint. Each registered webhook source has its own sourceId and transforms incoming payloads into Hogsend events.

Path Parameters

ParamTypeDescription
sourceIdstringRegistered webhook source identifier

Authentication -- Each source defines its own auth header. The value is matched against the corresponding environment variable.

Available Sources

PostHog (sourceId: posthog)

Receives events from PostHog webhook destinations and workflow batch triggers.

Auth header: X-PostHog-Webhook-Secret (matched against POSTHOG_WEBHOOK_SECRET env var)

Request Body

{
  "event": {
    "uuid": "event-uuid",
    "event": "user:signed_up",
    "distinct_id": "user_abc123",
    "timestamp": "2025-01-15T10:30:00.000Z",
    "properties": { "plan": "pro" }
  },
  "person": {
    "id": "person-uuid",
    "properties": {
      "email": "[email protected]"
    }
  }
}
FieldTypeRequiredDescription
event.eventstringYesPostHog event name
event.distinct_idstringYesPostHog distinct ID (maps to userId)
event.uuidstringNoPostHog event UUID (stored as _posthogEventId property)
event.timestampstringNoEvent timestamp
event.propertiesRecord<string, unknown>NoEvent properties
person.properties.emailstringNoUser email (maps to userEmail)

Response 200

{
  "ok": true,
  "event": "user:signed_up",
  "userId": "user_abc123",
  "exits": []
}

If the source's transform function returns null, the event is skipped:

{ "ok": true, "skipped": true }

Response 401 -- Invalid webhook secret.

Response 404 -- Unknown source ID.

Response 400 -- Payload failed schema validation.

curl -X POST http://localhost:3002/v1/webhooks/posthog \
  -H "Content-Type: application/json" \
  -H "X-PostHog-Webhook-Secret: your-posthog-secret" \
  -d '{
    "event": {
      "event": "user:signed_up",
      "distinct_id": "user_abc123",
      "properties": { "plan": "pro" }
    },
    "person": {
      "properties": { "email": "[email protected]" }
    }
  }'

Adding a Webhook Source

To add a new webhook source that transforms external payloads into Hogsend events:

// src/webhook-sources/my-source.ts
import { defineWebhookSource } from "./define-webhook-source.js";
import { z } from "zod";

const payloadSchema = z.object({
  action: z.string(),
  user_id: z.string(),
  user_email: z.string().optional(),
});

export const mySource = defineWebhookSource({
  meta: {
    id: "my-source",
    name: "My Source",
    description: "Receives events from My Source",
  },
  auth: {
    header: "x-my-source-secret",
    envKey: "MY_SOURCE_WEBHOOK_SECRET",
    type: "match",
  },
  schema: payloadSchema,
  async transform(payload) {
    return {
      event: `my_source:${payload.action}`,
      userId: payload.user_id,
      userEmail: payload.user_email ?? "",
      properties: {},
    };
  },
});

Then register it in src/webhook-sources/index.ts:

import { mySource } from "./my-source.js";

const allSources: DefinedWebhookSource[] = [posthogSource, mySource];

The source is now available at POST /v1/webhooks/my-source.

On this page