# @boe-ventures/platform-schemas

TypeScript types and endpoint documentation for unofficial messaging platform APIs.

## What This Is

A community-maintainable reference for the internal APIs that messaging platforms use under the hood. Not an API client — a **typed field manual and endpoint reference** built from reverse-engineering browser sessions.

The primary audience is humans and AI agents trying to understand what Hydra knows:
which endpoints exist, how auth is extracted, what is volatile, what is risky, and
which claims are live-verified versus merely documented. Some normalized types are
consumed directly by Hydra, but runtime SDK behavior is deliberately out of scope.

Each platform module documents:

- **API endpoints** — URLs, methods, required headers
- **Authentication** — how to extract tokens from a browser session (cookies, CSRF tokens, localStorage)
- **Raw response types** — what the API actually returns, as TypeScript interfaces
- **Normalization** — optional functions to convert raw responses into a common shape
- **Anti-detection** — rate limits, delay recommendations, known risks per platform
- **Volatile IDs** — GraphQL query IDs and doc_ids that change with deploys
- **Verification metadata** — whether an endpoint is live-verified, experimental, stale, or reference-only
- **Changelog** — observed API changes over time

## Why

These platforms don't offer official messaging/DM APIs — or lock them behind enterprise tiers. The only way to build on them is to ride the browser session, making the same API calls the web app makes.

The problem: these APIs change without notice. LinkedIn rotates GraphQL query IDs. Instagram changes doc_ids on deploy. When they change, your integration breaks.

This package documents what we've learned, in one place, with types.

## Platforms

| Platform | Auth Pattern | Read | Write | API Style | Stability |
|----------|-------------|------|-------|-----------|-----------|
| **Slack** | `xoxc-` token + `d` cookie | ✅ | ✅ | REST (internal) | Stable |
| **LinkedIn** | CSRF from `JSESSIONID` cookie | ✅ | ⚠️ experimental | GraphQL (Voyager) | ⚠️ Volatile query IDs |
| **Instagram** | CSRF cookie + `x-ig-app-id` | ✅ | — | REST + GraphQL | ⚠️ Volatile doc_ids, strictest detection |
| **Tinder** | `x-auth-token` (bearer) | ✅ | ✅ | REST | Stable |
| **iMessage** | Full Disk Access (macOS) | ✅ | ✅ | Local SQLite | Stable (schema changes across macOS versions) |

## Install

```bash
npm install @boe-ventures/platform-schemas
```

## Usage

### Read confidence metadata

Every endpoint can carry `verification` metadata. Agents should inspect this before
treating an endpoint as product-safe:

```ts
import { linkedinAdapter } from "@boe-ventures/platform-schemas";

console.log(linkedinAdapter.endpoints.sendMessage?.verification);
// {
//   status: "experimental",
//   lastChecked: "2026-06-25",
//   source: "external_reference",
//   notes: "Endpoint family cross-checked..."
// }
```

Status values:

| Status | Meaning |
|--------|---------|
| `live_verified` | Recently exercised by Hydra or a local platform path. |
| `documented` | Known/documented from capture or prior work, but not necessarily product-runtime. |
| `experimental` | Plausible and useful, but needs a controlled live test before broad use. |
| `reference_only` | Included to orient agents; do not treat as implemented. |
| `stale` | Known or suspected to be outdated. |

### Browse endpoint documentation

```ts
import { linkedinAdapter, slackAdapter } from "@boe-ventures/platform-schemas";

// What endpoint does LinkedIn use for conversations?
console.log(linkedinAdapter.endpoints.listConversations.urlTemplate);
// → https://www.linkedin.com/voyager/api/voyagerMessagingGraphQL/graphql?queryId=...

// What headers does it need?
console.log(linkedinAdapter.endpoints.listConversations.headers);
// → { "csrf-token": "{csrfToken}", "x-restli-protocol-version": "2.0.0", ... }

// How do you extract auth tokens?
console.log(linkedinAdapter.auth.sources);
// → [{ type: "cookie", name: "csrfToken", selector: "JSESSIONID", ... }]
```

### Check volatile IDs

When a platform integration breaks, check here first:

```ts
import { linkedinAdapter } from "@boe-ventures/platform-schemas";
import { DOC_IDS } from "@boe-ventures/platform-schemas/instagram";

// LinkedIn GraphQL query IDs (change periodically)
console.log(linkedinAdapter.volatileIds);
// → { conversations: "messengerConversations.0d5e6...", messages: "messengerMessages.5846e..." }

// Instagram doc_ids (change on deploy)
console.log(DOC_IDS);
// → { inboxQuery: "27228858046797698", threadDetail: "27530161873341603" }
```

### Use raw response types

```ts
import type { SlackBootResponse, SlackRawChannel } from "@boe-ventures/platform-schemas/slack";
import type { LinkedInRawConversation } from "@boe-ventures/platform-schemas/linkedin";
import type { TinderRawMatch } from "@boe-ventures/platform-schemas/tinder";

// Type your own API responses
const response: SlackBootResponse = await fetchSlackBoot();
const channels: SlackRawChannel[] = response.channels;
```

### Use built-in normalization (optional)

Each platform includes normalization functions that map raw responses to a common `Conversation` type. Use them or build your own:

```ts
import { normalizeConversation } from "@boe-ventures/platform-schemas/slack";
import type { Conversation } from "@boe-ventures/platform-schemas";

const conversation: Conversation = normalizeConversation(rawChannel, userMap, teamId, selfId);
```

### Anti-detection reference

```ts
import { instagramAdapter } from "@boe-ventures/platform-schemas";

console.log(instagramAdapter.antiDetection);
// → { minDelayMs: 2000, maxDelayMs: 5000, maxMessageThreads: 5,
//     notes: "Instagram is the STRICTEST platform..." }
```

## Fetching a single thread (targeted pull)

The inbox/list endpoints return a **thread list with only each thread's *last*
message** — not full history. To pull one conversation's messages on demand
(e.g. lazy-loading a person's thread), call the per-thread endpoint keyed by the
thread's id:

| Platform | Endpoint | Thread key | Notes |
|----------|----------|------------|-------|
| **Instagram** | **REST** `GET /api/v1/direct_v2/threads/{thread_id}/` (primary) — or GraphQL `IGDSlideAsyncFetchAndInsertIGDViewerThreadQuery` (`doc_id` `27110549851904846`) | REST: long `thread_id` · GraphQL: short `thread_fbid` | ⚠️ **Don't mix the two ids.** The inbox's long `thread_id` (`340282…`) works with REST but NOT GraphQL, which wants the short `thread_fbid` (`1360…`). REST is reliable since we already hold the `thread_id`. Non-text items (story replies, reactions, shares) carry no `text` — label by `item_type`. |
| **LinkedIn** | `messengerMessages` (Voyager GraphQL) | conversation URN `urn:li:msg_conversation:…` | URN is URL-encoded in the query `variables` |
| **Slack** | `POST /api/conversations.history` | `channel` (`C…`/`D…`/`G…` id) | same call the inbox loop makes, for one channel |
| **Tinder** | `GET /v2/matches?message=1` | — | matches already include recent messages inline (`message=0` returns none) |
| **iMessage** | local `chat.db` | `chat.ROWID` / `chat_identifier` | join `chat_message_join` → `message`; text may live in `attributedBody` (streamtyped) |

> ⚠️ The single most common extraction bug: reading the inbox's `last_message`
> and assuming you have the conversation. You don't — most threads need this
> per-thread call to actually get their messages.

## Sending messages (write)

Where supported, the web client sends via the same internal API. Writes execute
in the **browser's page context** (same origin, same session) — indistinguishable
from the user. Gate them behind explicit approval.

Write endpoints are documentation unless their `verification.status` says they are
live-verified in the current Hydra runtime. Even then: no bulk sending, no unattended
campaigns, and keep human cadence.

| Platform | Endpoint | Key payload | Auth |
|----------|----------|-------------|------|
| **Slack** | `POST /api/chat.postMessage` (multipart) | `channel`, `blocks` (`rich_text`) or `text`, `client_msg_id`, `draft_id` | `xoxc-` token + `d` cookie |
| **LinkedIn** | `POST /voyagerMessagingDashMessengerMessages?action=createMessage` | `conversationUrn`, `mailboxUrn`, `originToken`, `trackingId`, attributed text body | CSRF from `JSESSIONID`; run same-origin in a LinkedIn tab |
| **Tinder** | `POST /user/matches/{matchId}` | `{ message }` | `x-auth-token` |
| **Instagram** | GraphQL `IGDirectTextSendMutation` (`doc_id` `26911679871773184`) | `ig_thread_igid` (the short `thread_fbid`), `offline_threading_id`, `text.sensitive_string_value`, `send_attribution: "igd_web_chat_tab:in_thread"` | `x-csrftoken` + `fb_dtsg` + `x-fb-lsd` (from page config) |

## Current Coverage Notes

The normalized `Platform` union includes a few platforms that do not yet have
full adapter docs in this package:

| Platform | Current package status |
|----------|------------------------|
| `whatsapp` | Normalized types are consumed by the local Baileys sidecar; adapter docs live elsewhere for now. |
| `x` | Hydra extension has runtime work, but package docs are not promoted yet. |
| `homi` | Hydra-specific source; not a general messaging API package entry yet. |
| `beeper` | Intentionally adjacent, not a reverse-engineered platform API. Consider a future source-adapter note. |

## Contributing

When an API breaks:

1. Open the platform in your browser → Network tab
2. Navigate to the messaging/DM section
3. Find the new endpoint URL, query ID, or doc_id
4. Update the relevant file in `src/platforms/`
5. Update `volatileIds` and add a `changelog` entry
6. Open a PR

### What to capture

- **Endpoint URLs** — filter Network tab by `graphql`, `api`, `voyager`
- **Headers** — especially CSRF tokens, app IDs, protocol versions
- **Response shapes** — copy a response and type it
- **Query IDs** — for GraphQL platforms, the `queryId` or `doc_id` parameter

## License

MIT

---

Originally extracted from [Hydra](https://github.com/boe-ventures/hydra), a multi-platform conversation aggregator.
