> ## Documentation Index
> Fetch the complete documentation index at: https://docs.oneshotagent.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Message a Connection

> Send a message to a first-degree connection: into the conversation the account already has with them, or a new one.

Sends one message to a **person**. If the account already has a 1:1 conversation with them, the message goes into it. If not, OneShot starts one. Use it after an [invitation](/api-reference/linkedin/invite) is accepted, or for a connection the account has never messaged. To answer inside a conversation you already have the id of, use [Reply](/api-reference/linkedin/reply).

Paid: flat price per message (see [Pricing](/pricing)). Requires the `reply` grant and an `Idempotency-Key` header. It shares the reply daily cap and pacing: to LinkedIn, a reply and a message are the same thing.

<Warning>
  The human's account sends the message, and a message LinkedIn accepted **cannot be recalled**. Every call must carry an `Idempotency-Key`. The SDKs generate one if you omit it and reuse it on retries, so a retry never sends twice.
</Warning>

<Snippet file="audit-context-params.mdx" />

## Request

<ParamField header="Idempotency-Key" type="string" required>
  1–255 chars of `[A-Za-z0-9_.:-]`. A retry with the same key and body replays the accepted response instead of paying and sending again. The key is also stored on the action, so reuse is refused (`409 duplicate_request`, with the original `request_id`) even if the cache is unavailable.
</ParamField>

<ParamField body="account_id" type="string" required>Connected LinkedIn account id.</ParamField>
<ParamField body="profile" type="string" required>The connection. Takes what [Invite](/api-reference/linkedin/invite) takes: an HTTPS `linkedin.com/in/` profile URL, a slug or a provider id.</ParamField>
<ParamField body="text" type="string" required>1–4000 characters.</ParamField>

## Before payment

Each of these returns a non-2xx and is never settled:

| Status | `error` | Why |
| - | - | - |
| 400 | `idempotency_key_required` | No `Idempotency-Key` header. |
| 400 | `invalid_request` | Body failed validation. |
| 400 | `exceeds_caller_budget` | Price above your `X-Max-Cost-USDC`. |
| 404 | `account_not_found` | Not yours. |
| 403 | `grant_denied` / `grant_revoked` | The human did not grant `reply`, or revoked. |
| 409 | `account_not_ready` | Account is `reconnect_required` / `error`. |
| 400 | `content_blocked` | Content safety. |
| 429 | `account_send_limit` | Daily cap for this account reached, counting replies and messages together (`limit`, `used`, `pending`, `resets_at` in `details`). |
| 503 | `limits_unavailable` | Caps could not be verified, so the request fails closed. Retry shortly. |

Whether the person is a connection is checked at send time, not here. See **Outcome**.

## Response

```json theme={null}
{
  "request_id": "…", "receipt_id": "rcpt_…", "status": "processing", "tool": "linkedin",
  "linkedin": {
    "action_request_id": "…", "action": "message", "account_id": "…",
    "scheduled_for": null,
    "headroom": { "limit": 50, "used": 3, "pending": 1, "resets_at": "2026-10-09T00:00:00.000Z" }
  }
}
```

## Outcome

Poll `GET /v1/requests/{request_id}` (the SDK waits for you):

* **completed**: `{ "action_request_id", "status": "sent", "conversation_id", "message_id", "provider_message_id", "sent_at" }`. `conversation_id` is the 1:1 conversation the message went into, whether it existed or was just created. It then appears in [Conversations](/api-reference/linkedin/conversations) and [Messages](/api-reference/linkedin/messages), and you can keep the thread going with [Reply](/api-reference/linkedin/reply).
* **failed**, with an `error_code`. Two of them mean "defer", not "give up":

  * `not_connected`: the person is not a first-degree connection of the account. Send an [invitation](/api-reference/linkedin/invite) and try again once it is [accepted](/api-reference/linkedin/invitations).
  * `rate_limited`: LinkedIn throttled the account. Retry later with a new key.

  The others: `target_not_found` (no profile matches), `grant_revoked`, `account_disconnected`, `content_rejected`, `account_send_limit`. The failed-job sweep refunds messages that failed before sending.
* **processing with `send_status: "ambiguous"`**: the upstream timed out after the request may have been delivered. OneShot does not retry blindly. It looks for the message in the chat every few minutes, re-attempts once if there is still no trace after 10 minutes, and after 24 hours without confirmation fails the job with `send_unverified`.

## Example

```typescript theme={null}
const sent = await agent.linkedinMessage({
  accountId,
  profile: 'https://www.linkedin.com/in/jane-doe/',
  text: 'Thanks for connecting. Are you free for 15 minutes next week?',
  idempotencyKey: 'cadence:jane:first-message',
});
console.log(sent.conversation_id);
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.