Hogsend
API Reference

Contacts API

Contact CRUD operations, email preference management, and contact timeline.

All admin endpoints require the Authorization: Bearer <ADMIN_API_KEY> header.

Contacts

GET /v1/admin/contacts

List contacts with optional search and pagination. Results are ordered by lastSeenAt descending.

Query Parameters

ParamTypeDefaultDescription
limitnumber50Results per page (1-100)
offsetnumber0Pagination offset
searchstring--Search by email or externalId (case-insensitive)

Response 200

{
  "contacts": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "externalId": "user_abc123",
      "email": "[email protected]",
      "properties": { "plan": "pro" },
      "firstSeenAt": "2025-01-10T08:00:00.000Z",
      "lastSeenAt": "2025-01-15T10:30:00.000Z",
      "createdAt": "2025-01-10T08:00:00.000Z",
      "updatedAt": "2025-01-15T10:30:00.000Z"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
curl -H "Authorization: Bearer your-admin-api-key" \
  "http://localhost:3002/v1/admin/contacts?limit=10&search=example.com"

GET /v1/admin/contacts/{id}

Get a single contact by internal UUID or externalId, along with their email preferences.

Path Parameters

ParamTypeDescription
idstringContact UUID or externalId

Response 200

{
  "contact": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "externalId": "user_abc123",
    "email": "[email protected]",
    "properties": { "plan": "pro" },
    "firstSeenAt": "2025-01-10T08:00:00.000Z",
    "lastSeenAt": "2025-01-15T10:30:00.000Z",
    "createdAt": "2025-01-10T08:00:00.000Z",
    "updatedAt": "2025-01-15T10:30:00.000Z"
  },
  "preferences": {
    "id": "pref-uuid",
    "userId": "user_abc123",
    "email": "[email protected]",
    "unsubscribedAll": false,
    "suppressed": false,
    "bounceCount": 0,
    "categories": { "journey": true }
  }
}

The preferences field is null if no preference record exists for the contact.

Response 404

{ "error": "Contact not found" }
curl -H "Authorization: Bearer your-admin-api-key" \
  http://localhost:3002/v1/admin/contacts/user_abc123

POST /v1/admin/contacts

Create a new contact.

Request Body

{
  "externalId": "user_abc123",
  "email": "[email protected]",
  "properties": { "plan": "pro", "company": "Acme" }
}
FieldTypeRequiredDescription
externalIdstringYesYour system's user identifier (min 1 char)
emailstringNoEmail address (validated format)
propertiesRecord<string, unknown>NoArbitrary contact properties

Response 201

{
  "contact": { ... }
}

Response 409

{ "error": "Contact with this externalId already exists" }
curl -X POST http://localhost:3002/v1/admin/contacts \
  -H "Authorization: Bearer your-admin-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "externalId": "user_abc123",
    "email": "[email protected]",
    "properties": { "plan": "pro" }
  }'

PATCH /v1/admin/contacts/{id}

Update an existing contact. Properties are merged (deep merge via jsonb ||) rather than replaced.

Path Parameters

ParamTypeDescription
idstringContact UUID or externalId

Request Body

{
  "email": "[email protected]",
  "properties": { "plan": "enterprise" }
}
FieldTypeRequiredDescription
emailstringNoUpdated email address
propertiesRecord<string, unknown>NoProperties to merge into existing

Response 200 -- Updated contact object.

Response 404 -- Contact not found.

curl -X PATCH http://localhost:3002/v1/admin/contacts/user_abc123 \
  -H "Authorization: Bearer your-admin-api-key" \
  -H "Content-Type: application/json" \
  -d '{ "properties": { "plan": "enterprise" } }'

DELETE /v1/admin/contacts/{id}

Soft-delete a contact. This sets deletedAt on the contact record rather than permanently removing it. Soft-deleted contacts are excluded from all list queries, search results, and journey enrollment. Their associated email preferences and journey states are preserved.

Path Parameters

ParamTypeDescription
idstringContact UUID or externalId

Response 200

{ "deleted": true }

Response 404 -- Contact not found.

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

Email Preferences

These endpoints manage email preferences for a specific contact. They are mounted under the admin contacts router and require the same Authorization: Bearer <ADMIN_API_KEY> header.

GET /v1/admin/contacts/{contactId}/preferences

Get email preferences for a contact.

Path Parameters

ParamTypeDescription
contactIdstringContact UUID or externalId

Response 200

{
  "preferences": {
    "id": "pref-uuid",
    "userId": "user_abc123",
    "email": "[email protected]",
    "unsubscribedAll": false,
    "suppressed": false,
    "bounceCount": 0,
    "categories": { "journey": true },
    "suppressedAt": null,
    "lastBounceAt": null
  }
}
FieldTypeDescription
idstringPreference record UUID
userIdstringThe contact's externalId
emailstringEmail address
unsubscribedAllbooleanGlobal unsubscribe flag
suppressedbooleanWhether delivery is suppressed (e.g., due to bounces)
bounceCountnumberTotal hard bounces recorded
categoriesRecord<string, boolean>Per-category subscription status
suppressedAtstring | nullISO timestamp when suppression started
lastBounceAtstring | nullISO timestamp of most recent bounce

Response 404 -- Contact not found, or no preferences exist.

curl -H "Authorization: Bearer your-admin-api-key" \
  http://localhost:3002/v1/admin/contacts/user_abc123/preferences

PUT /v1/admin/contacts/{contactId}/preferences

Create or update email preferences for a contact. Uses upsert semantics -- if no preference record exists, one is created. The contact must have an email address on file.

Path Parameters

ParamTypeDescription
contactIdstringContact UUID or externalId

Request Body

{
  "unsubscribedAll": false,
  "suppressed": false,
  "categories": { "journey": true, "marketing": false }
}
FieldTypeRequiredDescription
unsubscribedAllbooleanNoSet global unsubscribe
suppressedbooleanNoSet delivery suppression
categoriesRecord<string, boolean>NoSet per-category preferences

Response 200 -- Updated preferences object.

Response 400

{ "error": "Contact has no email address" }

Response 404 -- Contact not found.

curl -X PUT http://localhost:3002/v1/admin/contacts/user_abc123/preferences \
  -H "Authorization: Bearer your-admin-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "unsubscribedAll": false,
    "categories": { "journey": true }
  }'

Contact Timeline

View a contact's full activity history across events, journeys, and emails. Requires the Authorization: Bearer <ADMIN_API_KEY> header.

GET /v1/admin/contacts/{id}/timeline

Get a chronological feed of all activity for a contact, interleaving events received, journey state changes, and emails sent. Results are ordered by timestamp descending.

Path Parameters

ParamTypeDescription
idstringContact UUID or externalId

Query Parameters

ParamTypeDefaultDescription
limitnumber50Results per page (1-100)
offsetnumber0Pagination offset
typestring--Filter by entry type: event, journey, email

Response 200

{
  "timeline": [
    {
      "type": "event",
      "timestamp": "2025-01-15T10:30:00.000Z",
      "data": {
        "id": "event-uuid",
        "event": "user:signed_up",
        "properties": { "plan": "pro" }
      }
    },
    {
      "type": "journey",
      "timestamp": "2025-01-15T10:30:01.000Z",
      "data": {
        "id": "state-uuid",
        "journeyId": "activation-welcome",
        "status": "completed",
        "currentNodeId": "done",
        "completedAt": "2025-01-16T10:30:00.000Z",
        "exitedAt": null
      }
    },
    {
      "type": "email",
      "timestamp": "2025-01-15T10:30:02.000Z",
      "data": {
        "id": "email-uuid",
        "templateKey": "activation/welcome",
        "subject": "Welcome to Hogsend",
        "status": "delivered",
        "toEmail": "[email protected]",
        "sentAt": "2025-01-15T10:30:02.000Z",
        "deliveredAt": "2025-01-15T10:30:07.000Z",
        "openedAt": null
      }
    }
  ],
  "total": 3,
  "limit": 50,
  "offset": 0
}

Each entry has a type discriminator and a data object whose shape varies by type:

TypeData fields
eventid, event, properties
journeyid, journeyId, status, currentNodeId, completedAt, exitedAt
emailid, templateKey, subject, status, toEmail, sentAt, deliveredAt, openedAt

Response 404 -- Contact not found.

curl -H "Authorization: Bearer your-admin-api-key" \
  "http://localhost:3002/v1/admin/contacts/user_abc123/timeline?type=email&limit=10"

On this page