Hogsend
API Reference

Operations API

Bulk import/export, batch enrollment, API key management, audit logs, alert rules, and dead letter queue.

Bulk Operations

Endpoints for bulk data operations. All require the Authorization: Bearer <api-key> header.

POST /v1/admin/contacts/import

Bulk import contacts from CSV or JSON. The import runs asynchronously via Hatchet.

Request Body

{
  "format": "csv",
  "data": "externalId,email,plan\nuser_001,[email protected],pro\nuser_002,[email protected],free",
  "fileName": "import-jan-2025.csv"
}
FieldTypeRequiredDescription
format"csv" | "json"YesData format
datastringYesRaw CSV or JSON string
fileNamestringNoDescriptive name for the import job

Response 202

{
  "jobId": "job-uuid",
  "status": "pending"
}
curl -X POST http://localhost:3002/v1/admin/contacts/import \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "format": "json",
    "data": "[{\"externalId\":\"user_001\",\"email\":\"[email protected]\"}]"
  }'

GET /v1/admin/contacts/import/{jobId}

Check the status of an import job.

Path Parameters

ParamTypeDescription
jobIdstringImport job UUID

Response 200

{
  "id": "job-uuid",
  "status": "completed",
  "totalRows": 500,
  "processedRows": 498,
  "failedRows": 2,
  "errors": [
    { "row": 42, "error": "Invalid email format" },
    { "row": 315, "error": "Duplicate externalId" }
  ]
}
FieldTypeDescription
idstringJob UUID
statusstringpending, processing, completed, or failed
totalRowsnumberTotal rows in the import
processedRowsnumberSuccessfully processed rows
failedRowsnumberRows that failed validation
errorsarrayPer-row error details
curl -H "Authorization: Bearer your-api-key" \
  http://localhost:3002/v1/admin/contacts/import/job-uuid

GET /v1/admin/contacts/export

Export contacts as JSON or CSV. Results stream directly in the response.

Query Parameters

ParamTypeDefaultDescription
formatstringjsonExport format: csv or json
searchstring--Filter by email or externalId
limitnumber10000Max rows to export (up to 10000)

Response 200 -- JSON array or CSV with Content-Disposition: attachment header.

# Export as CSV
curl -H "Authorization: Bearer your-api-key" \
  "http://localhost:3002/v1/admin/contacts/export?format=csv&limit=5000" \
  -o contacts.csv

# Export as JSON
curl -H "Authorization: Bearer your-api-key" \
  "http://localhost:3002/v1/admin/contacts/export?format=json&search=example.com"

POST /v1/admin/journeys/{id}/enroll/batch

Batch enroll multiple users into a journey. Each user is processed through the standard ingestion pipeline with all entry guards applied.

Path Parameters

ParamTypeDescription
idstringJourney ID

Request Body

{
  "users": [
    { "userId": "user_001", "userEmail": "[email protected]", "properties": { "plan": "pro" } },
    { "userId": "user_002", "userEmail": "[email protected]" },
    { "userId": "user_003", "userEmail": "[email protected]" }
  ]
}
FieldTypeRequiredDescription
usersarrayYesUsers to enroll (max 500)
users[].userIdstringYesExternal user identifier
users[].userEmailstringYesUser email address
users[].propertiesRecord<string, unknown>NoAdditional event properties

Response 200

{
  "enrolled": 2,
  "skipped": 1,
  "results": [
    { "userId": "user_001", "enrolled": true },
    { "userId": "user_002", "enrolled": true },
    { "userId": "user_003", "enrolled": false }
  ]
}
FieldTypeDescription
enrollednumberCount of users successfully dispatched
skippednumberCount of users skipped (already enrolled, unsubscribed, etc.)
resultsarrayPer-user enrollment result
curl -X POST http://localhost:3002/v1/admin/journeys/activation-welcome/enroll/batch \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "users": [
      { "userId": "user_001", "userEmail": "[email protected]" },
      { "userId": "user_002", "userEmail": "[email protected]" }
    ]
  }'

API Keys

Manage database-backed API keys with scoped permissions. All require the Authorization: Bearer <api-key> header with full-admin scope.

GET /v1/admin/api-keys

List API keys. Revoked keys are excluded by default.

Query Parameters

ParamTypeDefaultDescription
limitnumber50Results per page
offsetnumber0Pagination offset
includeRevokedbooleanfalseInclude revoked keys

Response 200

{
  "keys": [
    {
      "id": "key-uuid",
      "name": "CI Pipeline",
      "keyPrefix": "hsk_abc1",
      "scopes": ["read"],
      "expiresAt": "2025-06-01T00:00:00.000Z",
      "revokedAt": null,
      "lastUsedAt": "2025-01-15T10:30:00.000Z",
      "createdAt": "2025-01-01T00:00:00.000Z"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
curl -H "Authorization: Bearer your-api-key" \
  "http://localhost:3002/v1/admin/api-keys?includeRevoked=true"

POST /v1/admin/api-keys

Create a new API key. The raw key is only returned in this response and cannot be retrieved again.

Request Body

{
  "name": "CI Pipeline",
  "scopes": ["read"],
  "expiresAt": "2025-06-01T00:00:00.000Z"
}
FieldTypeRequiredDescription
namestringYesHuman-readable key name
scopesstring[]YesPermission scopes: read, journey-admin, full-admin
expiresAtstringNoISO 8601 expiry datetime

Response 201

{
  "id": "key-uuid",
  "name": "CI Pipeline",
  "key": "hsk_abc123def456ghi789...",
  "keyPrefix": "hsk_abc1",
  "scopes": ["read"],
  "expiresAt": "2025-06-01T00:00:00.000Z",
  "createdAt": "2025-01-15T10:30:00.000Z"
}

The key field contains the full API key. Store it securely -- it is SHA-256 hashed before storage and cannot be retrieved again.

curl -X POST http://localhost:3002/v1/admin/api-keys \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "CI Pipeline",
    "scopes": ["read"],
    "expiresAt": "2025-06-01T00:00:00.000Z"
  }'

DELETE /v1/admin/api-keys/{id}

Revoke an API key. The key is immediately invalidated and removed from the auth cache.

Path Parameters

ParamTypeDescription
idstringAPI key UUID

Response 200

{ "revoked": true }

Response 404 -- Key not found.

curl -X DELETE http://localhost:3002/v1/admin/api-keys/key-uuid \
  -H "Authorization: Bearer your-api-key"

Audit Logs

Query the audit trail of all admin mutations. Every POST, PUT, PATCH, and DELETE request to admin endpoints is automatically recorded. Requires the Authorization: Bearer <api-key> header.

GET /v1/admin/audit-logs

List audit log entries with optional filters.

Query Parameters

ParamTypeDefaultDescription
limitnumber50Results per page
offsetnumber0Pagination offset
actorstring--Filter by actor (key name or "legacy")
resourcestring--Filter by resource type (e.g., contact, journey, api-key)
actionstring--Filter by action (e.g., create, update, delete)
fromstring--ISO 8601 datetime lower bound
tostring--ISO 8601 datetime upper bound

Response 200

{
  "logs": [
    {
      "id": "log-uuid",
      "actor": "CI Pipeline",
      "actorKeyId": "key-uuid",
      "action": "create",
      "resource": "contact",
      "resourceId": "contact-uuid",
      "detail": { "externalId": "user_abc123" },
      "ipAddress": "192.168.1.1",
      "createdAt": "2025-01-15T10:30:00.000Z"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
FieldTypeDescription
idstringLog entry UUID
actorstringKey name or "legacy" for env-var key
actorKeyIdstring | nullAPI key UUID (null for legacy key)
actionstringAction performed: create, update, delete, revoke, enroll, cancel, import, export, replay, resend
resourcestringResource type: contact, journey, api-key, alert-rule, email, event, dlq
resourceIdstring | nullTarget resource identifier
detailobject | nullAdditional context about the action
ipAddressstringClient IP address
createdAtstringISO 8601 timestamp
curl -H "Authorization: Bearer your-api-key" \
  "http://localhost:3002/v1/admin/audit-logs?resource=contact&action=delete&from=2025-01-01T00:00:00Z"

Alerts

Configure alert rules to be notified when metrics cross thresholds. All require the Authorization: Bearer <api-key> header.

GET /v1/admin/alerts/rules

List all alert rules.

Response 200

{
  "rules": [
    {
      "id": "rule-uuid",
      "name": "High bounce rate",
      "type": "bounce_rate_exceeded",
      "threshold": 0.05,
      "channel": "webhook",
      "channelConfig": { "url": "https://hooks.slack.com/..." },
      "cooldownMinutes": 60,
      "enabled": true,
      "lastTriggeredAt": "2025-01-14T08:00:00.000Z",
      "createdAt": "2025-01-01T00:00:00.000Z",
      "updatedAt": "2025-01-01T00:00:00.000Z"
    }
  ]
}
curl -H "Authorization: Bearer your-api-key" \
  http://localhost:3002/v1/admin/alerts/rules

POST /v1/admin/alerts/rules

Create a new alert rule.

Request Body

{
  "name": "High bounce rate",
  "type": "bounce_rate_exceeded",
  "threshold": 0.05,
  "channel": "webhook",
  "channelConfig": { "url": "https://hooks.slack.com/services/..." },
  "cooldownMinutes": 60
}
FieldTypeRequiredDescription
namestringYesHuman-readable rule name
typestringYesAlert type (see below)
thresholdnumberYesNumeric threshold that triggers the alert
channelstringYesNotification channel: webhook, slack, or email
channelConfigobjectYesChannel-specific configuration
cooldownMinutesnumberNoMinimum minutes between alerts (default: 60)

Alert Types

TypeThreshold meaning
bounce_rate_exceededBounce rate exceeds this ratio (e.g., 0.05 = 5%)
journey_failure_spikeNumber of journey failures in the last hour
delivery_issueDelivery rate drops below this ratio
high_complaint_rateComplaint rate exceeds this ratio

Channel Config

ChannelConfig fields
webhook{ url: string }
slack{ webhookUrl: string, channel?: string }
email{ to: string }

Response 201 -- Created alert rule.

curl -X POST http://localhost:3002/v1/admin/alerts/rules \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "High bounce rate",
    "type": "bounce_rate_exceeded",
    "threshold": 0.05,
    "channel": "slack",
    "channelConfig": { "webhookUrl": "https://hooks.slack.com/services/..." },
    "cooldownMinutes": 60
  }'

PATCH /v1/admin/alerts/rules/{id}

Update an alert rule. Any field can be updated.

Path Parameters

ParamTypeDescription
idstringAlert rule UUID

Request Body -- Same fields as POST, all optional.

Response 200 -- Updated alert rule.

Response 404 -- Rule not found.

curl -X PATCH http://localhost:3002/v1/admin/alerts/rules/rule-uuid \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{ "threshold": 0.03, "cooldownMinutes": 120 }'

DELETE /v1/admin/alerts/rules/{id}

Delete an alert rule.

Path Parameters

ParamTypeDescription
idstringAlert rule UUID

Response 200

{ "deleted": true }

Response 404 -- Rule not found.

curl -X DELETE http://localhost:3002/v1/admin/alerts/rules/rule-uuid \
  -H "Authorization: Bearer your-api-key"

GET /v1/admin/alerts/history

List past alert triggers.

Query Parameters

ParamTypeDefaultDescription
limitnumber50Results per page
offsetnumber0Pagination offset
ruleIdstring--Filter by rule UUID

Response 200

{
  "alerts": [
    {
      "id": "alert-uuid",
      "ruleId": "rule-uuid",
      "ruleName": "High bounce rate",
      "type": "bounce_rate_exceeded",
      "currentValue": 0.062,
      "threshold": 0.05,
      "channel": "slack",
      "delivered": true,
      "triggeredAt": "2025-01-15T08:00:00.000Z"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
curl -H "Authorization: Bearer your-api-key" \
  "http://localhost:3002/v1/admin/alerts/history?limit=10"

Dead Letter Queue

Inspect and manage failed tasks that have been moved to the dead letter queue. All require the Authorization: Bearer <api-key> header.

GET /v1/admin/dlq

List DLQ entries with optional filters.

Query Parameters

ParamTypeDefaultDescription
limitnumber50Results per page
offsetnumber0Pagination offset
sourcestring--Filter by source (e.g., email, journey, webhook)
statusstring--Filter by status: pending, retried, discarded

Response 200

{
  "entries": [
    {
      "id": "dlq-uuid",
      "source": "email",
      "sourceId": "email-uuid",
      "payload": { "templateKey": "activation/welcome", "toEmail": "[email protected]" },
      "error": "Resend API timeout after 3 retries",
      "retryCount": 3,
      "status": "pending",
      "retriedAt": null,
      "createdAt": "2025-01-15T10:30:00.000Z"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
FieldTypeDescription
idstringDLQ entry UUID
sourcestringOrigin of the failed task
sourceIdstring | nullRelated resource ID
payloadobjectOriginal task payload
errorstringError message from the last attempt
retryCountnumberNumber of retry attempts
statusstringpending, retried, or discarded
retriedAtstring | nullISO timestamp of last retry
createdAtstringISO timestamp when the entry was created
curl -H "Authorization: Bearer your-api-key" \
  "http://localhost:3002/v1/admin/dlq?status=pending&source=email"

POST /v1/admin/dlq/{id}/retry

Mark a DLQ entry for retry. The task will be re-queued through its original pipeline.

Path Parameters

ParamTypeDescription
idstringDLQ entry UUID

Response 200

{
  "id": "dlq-uuid",
  "status": "retried",
  "retriedAt": "2025-01-15T11:00:00.000Z"
}

Response 404 -- Entry not found.

Response 409 -- Entry already retried or discarded.

curl -X POST http://localhost:3002/v1/admin/dlq/dlq-uuid/retry \
  -H "Authorization: Bearer your-api-key"

DELETE /v1/admin/dlq/{id}

Discard a DLQ entry. The task will not be retried.

Path Parameters

ParamTypeDescription
idstringDLQ entry UUID

Response 200

{
  "id": "dlq-uuid",
  "status": "discarded"
}

Response 404 -- Entry not found.

curl -X DELETE http://localhost:3002/v1/admin/dlq/dlq-uuid \
  -H "Authorization: Bearer your-api-key"

DI Container Pattern

The API uses a dependency-injection container that is created once at startup and set on every request context. Handlers access it via c.get("container").

// Container interface
interface Container {
  env: typeof env;          // Validated environment variables
  logger: Logger;           // Structured logger (winston)
  db: Database;             // Drizzle ORM instance
  dbClient: DatabaseClient; // Raw pg client
  auth: Auth;               // Better Auth instance
  email: Resend;            // Resend client
  emailService: EmailService | null; // Email service (requires RESEND_WEBHOOK_SECRET)
  registry: JourneyRegistry;        // Journey registry
  hatchet: HatchetClient;           // Hatchet task orchestration client
}

In route handlers, destructure what you need:

app.openapi(myRoute, async (c) => {
  const { db, logger, hatchet } = c.get("container");
  // ...
});

The AppEnv type defines what is available on the Hono context:

type AppEnv = {
  Variables: {
    container: Container;
    requestId: string;
    user: User | null;      // Set by auth middleware
    session: Session | null; // Set by auth middleware
  };
};

On this page