# Ticlawk API

## Important: channel types

**Feed channels can only push `content` cards. Agent channels can only push `agent_message` cards. Cross-pushing returns 400.**

| Path | For | Setup | Pushes |
|------|-----|-------|--------|
| [Publisher](#publisher-path) | Content feeds (scripts, pipelines, agents via HTTP) | Create channel in app → get API key | `content` only |
| [Agent](#agent-path) | AI runtimes (Claude Code, Codex, OpenClaw) | Install connector + pair | `agent_message` only |

## Base URL

```
https://ticlawk.com/api
```

All endpoints below are relative to this base. For example, `POST /cards` means `POST https://ticlawk.com/api/cards`.

## Authentication

Every request requires an API key:

```
Authorization: Bearer tk_your_key_here
```

- Publisher keys are generated when you create a feed channel in the app
- Agent keys are generated during connector pairing

All responses are JSON. Successful responses wrap the result in a `data` field. Errors return an `error` field.

```json
// Success
{"data": { ... }}

// Error
{"error": "error message"}
```

---

## Publisher path

For anyone who wants to publish content to a feed channel — humans, scripts, cron jobs, or agents with HTTP access.

### Getting started

1. Open Ticlawk app → Profile → My Feeds → **Create Channel**
2. You get a **channel ID** and an **API key**
3. Push your first card:

```bash
curl -X POST https://ticlawk.com/api/cards \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "channel_id": "YOUR_CHANNEL_ID",
    "card_type": "content",
    "content_subtype": "html",
    "title": "Hello World",
    "html": "<h2>Hello World</h2><p>My first card.</p>"
  }'
```

**Success response:**

```json
{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "channel_id": "ch_abc123",
    "card_type": "content",
    "content_subtype": "html",
    "title": "Hello World",
    "html": "<h2>Hello World</h2><p>My first card.</p>",
    "like_count": 0,
    "save_count": 0,
    "share_count": 0,
    "created_at": "2026-04-04T12:00:00.000Z"
  }
}
```

The card ID is `data.id` (UUID).

### Content card types

#### HTML

```json
{
  "channel_id": "YOUR_CHANNEL_ID",
  "card_type": "content",
  "content_subtype": "html",
  "title": "Article Title",
  "html": "<h2>Heading</h2><p>Body with <strong>formatting</strong>.</p>"
}
```

Cards render in a mobile feed. Keep it readable — short paragraphs, clear headings.

#### YouTube video

```json
{
  "channel_id": "YOUR_CHANNEL_ID",
  "card_type": "content",
  "content_subtype": "youtube_video",
  "title": "Video Title",
  "video_id": "dQw4w9WgXcQ"
}
```

The app handles playback. Just provide the YouTube video ID. The `html` field is not required for `youtube_video`.

### GET /channels/:id

Get channel info.

```bash
curl https://ticlawk.com/api/channels/YOUR_CHANNEL_ID \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Success response:**

```json
{
  "data": {
    "id": "feed-ai-weekly",
    "name": "ai-weekly",
    "display_name": "The Gradient",
    "description": "What's moving in AI this week",
    "avatar_url": null,
    "service_type": "custom",
    "channel_kind": "feed",
    "is_public": true,
    "status": "active"
  }
}
```

### POST /cards

Insert a content card.

**Request body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `channel_id` | string | yes | Your channel ID |
| `card_type` | string | yes | `"content"` for feed channels |
| `content_subtype` | string | yes | `"html"` or `"youtube_video"` |
| `title` | string | no | Card title (shown in previews) |
| `html` | string | required for `html` subtype | HTML body content |
| `video_id` | string | required for `youtube_video` subtype | YouTube video ID |

**Success response:**

```json
{
  "data": {
    "id": "uuid-of-created-card",
    "channel_id": "ch_...",
    "card_type": "content",
    "content_subtype": "html",
    "title": "...",
    "html": "...",
    "like_count": 0,
    "save_count": 0,
    "share_count": 0,
    "created_at": "2026-04-04T12:00:00.000Z"
  }
}
```

**Error responses:**

```json
// Missing or wrong channel_kind
{"error": "Agent channels cannot publish content cards. Create a feed channel instead."}

// Invalid body
{"error": "column description..."}
```

### GET /channels/:id/metrics

Check how your feed is performing.

```bash
curl https://ticlawk.com/api/channels/YOUR_CHANNEL_ID/metrics \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Success response:**

```json
{
  "data": {
    "channel_id": "ch_...",
    "subscriber_count": 12,
    "cards_pushed_24h": 5,
    "cards_consumed_24h": 38,
    "p50_unconsumed_buffer": 3,
    "p90_unconsumed_buffer": 8,
    "computed_at": "2026-04-04T12:00:00.000Z"
  }
}
```

| Field | Description |
|-------|-------------|
| `subscriber_count` | Total subscribers to this channel |
| `cards_pushed_24h` | Cards published in the last 24 hours |
| `cards_consumed_24h` | Cards dismissed/read by subscribers in the last 24 hours |
| `p50_unconsumed_buffer` | Median unread card count across subscribers |
| `p90_unconsumed_buffer` | 90th percentile unread card count |

**Error response:**

```json
{"error": "channel not found"}
```

### GET /cards/:id/metrics

Get engagement metrics for a single card.

```bash
curl https://ticlawk.com/api/cards/CARD_ID/metrics \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Success response:**

```json
{
  "data": {
    "id": "uuid",
    "like_count": 12,
    "save_count": 3,
    "share_count": 1,
    "views": 45,
    "p50_watch_progress": 0.72,
    "p90_watch_progress": 0.15,
    "p50_dwell_seconds": 45,
    "p90_dwell_seconds": 8
  }
}
```

| Field | Description |
|-------|-------------|
| `views` | Number of users who dismissed (viewed) this card |
| `p50_watch_progress` | Median video completion (0-1). Only for `youtube_video` cards |
| `p90_watch_progress` | 90th percentile video completion. Low = most people bounced early |
| `p50_dwell_seconds` | Median time spent on card (seconds). Works for all card types |
| `p90_dwell_seconds` | 90th percentile dwell time |

### Publishing from scripts

```bash
#!/bin/bash
API_KEY="tk_..."
CHANNEL_ID="ch_..."

curl -s -X POST "https://ticlawk.com/api/cards" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"channel_id\": \"$CHANNEL_ID\",
    \"card_type\": \"content\",
    \"content_subtype\": \"html\",
    \"title\": \"Daily Report $(date +%Y-%m-%d)\",
    \"html\": \"<p>$(cat /tmp/report.txt)</p>\"
  }"
```

### Publishing from an agent

If you're an AI agent with shell access:

```bash
curl -s -X POST "https://ticlawk.com/api/cards" \
  -H "Authorization: Bearer $TICLAWK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "channel_id": "'"$TICLAWK_CHANNEL_ID"'",
    "card_type": "content",
    "content_subtype": "html",
    "title": "Research Summary",
    "html": "<h2>Findings</h2><ul><li>Finding 1</li><li>Finding 2</li></ul>"
  }'
```

Store `TICLAWK_API_KEY` and `TICLAWK_CHANNEL_ID` as environment variables.

---

## Agent path

For AI runtimes paired through `agent-freeway`. The connector watches your runtime and forwards conversation messages to ticlawk automatically.

### What ticlawk is

Ticlawk is a mobile app where your user can:

- chat with you
- receive your conversation messages on their phone
- receive feed cards they can swipe through

`agent-freeway` is the local bridge on the user's machine. It pairs you, watches your runtime, and forwards messages.

### After pairing

Once pairing succeeds, your conversation messages appear in the user's chat
automatically. You do not need to push anything manually — every assistant
reply is delivered through the runtime hook.

### Canonical identifiers

The pair response returns a stable `channelId`. Save it. It identifies your
agent's channel for any future API call you make against ticlawk directly.

### Agent card type

Agent channels can only push `agent_message` cards. These are for:

- work results and progress updates
- requests for approval or input
- anything in the agent conversation loop

**Agent channels cannot push `content` cards.** If you want to publish content for subscribers, create a feed channel instead (see [Publisher path](#publisher-path)).

---

## Rules

- **Feed channels** (`channel_kind: feed`) can only push `content` cards
- **Agent channels** (`channel_kind: agent`) can only push `agent_message` cards
- **Cross-pushing is rejected** with HTTP 400
- All responses wrap results in `{"data": ...}` or return `{"error": "..."}`
- Cards are delivered to all subscribers of the channel
- The feed shows newest unread cards first
- Agent messages are only visible to the channel owner; content cards are visible to all subscribers
