# Mentions

4 endpoints.

## Search a platform for accounts to @mention

```http
GET https://orbitwrite.com/api/v1/mentions/handles
```

Requires `account:read`. Type-ahead over the platforms whose mentions are plain handles — X, Bluesky and Mastodon. Insert the returned `handle` after an `@` in the post body and the platform resolves it at publish time; no id or markup is needed. The workspace's own directory answers first (its channels, handles remembered from earlier picks, people from the inbox, @handles in its posts), then the network: Bluesky's and Mastodon's own search, or for X an exact lookup of the whole query as a handle — X has no affordable name search, so on X a partial name only matches people the workspace already knows. Mastodon handles are returned qualified (`user@instance`). A Mastodon search runs against that account's own instance on its token, so two channels can return different results for the same query. Channels on other platforms are a 400 — Threads has no handle search, and LinkedIn mentions are structured nodes (use `/mentions/linkedin`). A Bluesky or Mastodon search that can't reach the network is a 502; an X lookup that fails or is out of budget just adds nothing.

**Authentication:** bearer token or session cookie.

### Query parameters

- `accountId` (string, required) — The connected channel the mention is being written for. Names the workspace, and on Mastodon supplies the instance and token the search runs against.
- `q` (string, required) — What the author has typed after the `@`.

### Responses

#### 200

Matching accounts, best first, capped at 8

- `results` (HandleMention[], required)
  - `handle` (string, required) — Inserted verbatim after `@`. A username on X (`jack`), a domain on Bluesky (`alice.bsky.social`), `user@instance` on Mastodon.
  - `displayName` (string, optional)
  - `avatarUrl` (string, optional)
  - `source` (string, optional, one of `channel`, `remembered`, `inbox`, `recent`, `search`, `lookup`) — Where the candidate came from. `channel`: one of the workspace's own accounts. `remembered`: picked from this typeahead before (see `POST /mentions/remembered`). `inbox`: someone who replied to or mentioned the workspace. `recent`: an @handle in one of the workspace's posts on this network. `search`: the network's own typeahead (Bluesky, Mastodon). `lookup`: X's exact match for the whole query, at most one, appended last. The first four are the workspace's own data.
  - `contactId` (string, optional) — The directory contact this handle belongs to, when an author has linked it to handles on other networks.
  - `identities` (object[], optional) — The same person's handles on other networks, as linked by an author. Omitted when there are none.
    - `platform` (string, required)
    - `handle` (string, required)
    - `displayName` (string, optional)

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 404

The resource does not exist

- `error` (string, required)
- `issues` (any[], optional)

#### 429

Rate limit exceeded

- `error` (string, required)
- `issues` (any[], optional)

#### 502

The platform couldn't be reached

- `error` (string, required)

## Resolve a LinkedIn company slug to its organization URN

```http
GET https://orbitwrite.com/api/v1/mentions/linkedin/resolve
```

Requires `account:read`. Turns the vanity name in a company URL (`linkedin.com/company/orbitwrite` → `orbitwrite`) into the `urn:li:organization:…` a LinkedIn mention node needs, asking LinkedIn with the named channel's token. The lookup is exact-match: a slug LinkedIn has no company for returns `{ "organization": null }`, which is an answer and not an error — fall back to a numeric company URL or a URN the author supplies. A 502 means the lookup itself failed and the slug is still unknown; do not write a mention on the strength of one.

**Authentication:** bearer token or session cookie.

### Query parameters

- `accountId` (string, required) — The connected channel the mention is being written for. Names the workspace, and on Mastodon supplies the instance and token the search runs against.
- `slug` (string, required) — The vanity name from the company URL, without the leading path.

### Responses

#### 200

The organization, or `null` when LinkedIn has no company with that vanity name

- `organization` (object | any | null, required)

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 404

The resource does not exist

- `error` (string, required)
- `issues` (any[], optional)

#### 429

Rate limit exceeded

- `error` (string, required)
- `issues` (any[], optional)

#### 502

LinkedIn couldn't be reached

- `error` (string, required)

## Search LinkedIn mention candidates

```http
GET https://orbitwrite.com/api/v1/mentions/linkedin
```

Requires `account:read`. LinkedIn mentions are structured URN nodes rather than plain handles, so build the mention from the returned `id` — there is nothing useful to type.

Four directories, distinguished by `source`. `channel` results are the workspace's own connected LinkedIn accounts, substring-matched. `recent` results are people and companies the workspace has already @mentioned in LinkedIn posts written through Orbitwrite (never imported ones), most-used first. `follower` results are members who follow one of the workspace's Pages, found by LinkedIn's People Typeahead: from a Page channel, that Page's followers; from a personal channel, the followers of up to two of the workspace's Pages. Only Pages whose connection granted `r_organization_followers` are asked (reconnect a Page connected before Orbitwrite asked for it), and only for queries in LinkedIn's grammar — letters plus at most one space and single `'`, `-` or `.`, at least 3 characters (6 with a space or dot). LinkedIn returns only followers who allow mentions. A `lookup` result is a company LinkedIn itself returned, and appears only when the whole query is a well-formed vanity name — their keyword company search is restricted to selected developers, so exact match on the vanity name is the only search available to us. Typing `orbit` will not find `orbitwrite`; typing `orbitwrite` will. Queries shorter than 3 characters never reach LinkedIn, and a client should debounce: the quota behind this is per *application* per day, not per caller, so every prefix you send costs everyone.

A LinkedIn that can't be reached costs you the `follower` and `lookup` candidates, not the request. LinkedIn offers no search over a member's connections, so a personal channel finds people only through the workspace's own channels, its earlier mentions and its Pages' followers.

**Authentication:** bearer token or session cookie.

### Query parameters

- `accountId` (string, required) — The connected channel the mention is being written for. Names the workspace, and on Mastodon supplies the instance and token the search runs against.
- `q` (string, required) — What the author has typed after the `@`.

### Responses

#### 200

Matching mention targets, capped at 8 in total — a `lookup` result displaces the last channel rather than extending the list.

- `results` (LinkedInMention[], required)
  - `id` (string, required) — The URN to address: `urn:li:organization:123` for a page, `urn:li:person:<sub>` for a person.
  - `name` (string, required)
  - `avatarUrl` (string, optional)
  - `type` (string, required, one of `person`, `organization`)
  - `headline` (string, optional) — The member's LinkedIn headline, `follower` results only — what tells two people with the same name apart. Followers carry no photo.
  - `source` (string, required, one of `channel`, `recent`, `follower`, `lookup`) — `channel` is a LinkedIn account connected to this workspace, substring-matched. `recent` is a person or company this workspace has already @mentioned in a LinkedIn post written through Orbitwrite, substring-matched on the label last used. `follower` is a member who follows one of the workspace's Pages, prefix-matched on first or last name by LinkedIn. `lookup` is a company LinkedIn returned for the query taken as a vanity name — at most one, only when the query is a complete vanity name. Don't blend the two in a UI: a `lookup` row appears only on the last keystroke, which reads as a glitch inside a list that has been filtering all along.
  - `url` (string, optional) — Canonical linkedin.com URL, organizations only. Cross-posting a mention to a non-LinkedIn network degrades to this link; people have no equivalent public URL derivable from the URN.

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 404

The resource does not exist

- `error` (string, required)
- `issues` (any[], optional)

#### 429

Rate limit exceeded

- `error` (string, required)
- `issues` (any[], optional)

## Remember a handle for the mention typeahead

```http
POST https://orbitwrite.com/api/v1/mentions/remembered
```

Requires `post:create`. Adds a handle to the workspace's mention directory, or counts another use of one already there, so `GET /mentions/handles` offers it to everyone in the workspace from then on without asking the network. `accountId` is the X, Bluesky or Mastodon channel the mention is written from: it names the network, and a bare Mastodon username is qualified with that channel's instance. The handle must be well-formed for the network (a leading `@` is ignored). `displayName` and `avatarUrl` are optional, and when omitted the stored ones are kept; `avatarUrl` must be https.

**Authentication:** bearer token or session cookie.

### Request body (`application/json`)

- `accountId` (string, required)
- `handle` (string, required)
- `displayName` (string, optional)
- `avatarUrl` (string, optional)

### Responses

#### 200

The directory entry as stored, with `source: "remembered"`

- `mention` (HandleMention, required)
  - `handle` (string, required) — Inserted verbatim after `@`. A username on X (`jack`), a domain on Bluesky (`alice.bsky.social`), `user@instance` on Mastodon.
  - `displayName` (string, optional)
  - `avatarUrl` (string, optional)
  - `source` (string, optional, one of `channel`, `remembered`, `inbox`, `recent`, `search`, `lookup`) — Where the candidate came from. `channel`: one of the workspace's own accounts. `remembered`: picked from this typeahead before (see `POST /mentions/remembered`). `inbox`: someone who replied to or mentioned the workspace. `recent`: an @handle in one of the workspace's posts on this network. `search`: the network's own typeahead (Bluesky, Mastodon). `lookup`: X's exact match for the whole query, at most one, appended last. The first four are the workspace's own data.
  - `contactId` (string, optional) — The directory contact this handle belongs to, when an author has linked it to handles on other networks.
  - `identities` (object[], optional) — The same person's handles on other networks, as linked by an author. Omitted when there are none.
    - `platform` (string, required)
    - `handle` (string, required)
    - `displayName` (string, optional)

#### 400

Invalid request

- `error` (string, required)
- `issues` (any[], optional)

#### 401

Not signed in

- `error` (string, required)
- `issues` (any[], optional)

#### 403

Not a member of the organization

- `error` (string, required)
- `issues` (any[], optional)

#### 404

The resource does not exist

- `error` (string, required)
- `issues` (any[], optional)

#### 429

Rate limit exceeded

- `error` (string, required)
- `issues` (any[], optional)
