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

# Quickstart

> Publish your first tweet via curl in 90 seconds.

In 90 seconds you'll publish to X (Twitter) from your terminal. The same `POST /v1/posts` shape works for every platform we support — Twitter is the entry-point because most developers already think in tweets.

<Note>
  Twitter and Bluesky are live today. The remaining platforms (Instagram, LinkedIn, Facebook, Threads, Pinterest) are gated on their developer-portal reviews and turn on as approvals clear. Same API, same error envelope, same idempotency contract — substitute any connected `accountId` below if you're starting on a different platform.
</Note>

## 1. Get an API key

Sign in at [dashboard.letmepost.dev](https://dashboard.letmepost.dev) and mint a key from the **API keys** page. Keys look like `lmp_live_…` (production) or `lmp_test_…` (sandbox). Treat them like passwords; the dashboard only shows the secret once.

```bash theme={"system"}
export LMP_KEY=lmp_live_...
```

## 2. Connect Twitter

Open the dashboard's **Accounts** page and click **Connect X / Twitter**. The OAuth 2.0 PKCE flow is fully handled for you — you grant scopes once and we manage refresh transparently afterwards. When you come back, the account row will show the connected handle and an `id` you'll send posts against.

<Note>
  Each platform has its own connect flow (OAuth, PKCE, or app password for Bluesky). They're all documented per platform under [Platforms](/platforms-overview), but the dashboard is the easiest path for the first connection — it manages redirects and stores tokens encrypted.
</Note>

Copy the connected account's `id` from the dashboard. You'll need it in the next step.

## 3. Send a tweet

<CodeGroup>
  ```bash cURL theme={"system"}
  curl -X POST https://api.letmepost.dev/v1/posts \
    -H "Authorization: Bearer $LMP_KEY" \
    -H "Idempotency-Key: $(uuidgen)" \
    -H "Content-Type: application/json" \
    --data @- <<EOF
  {
    "targets": [
      { "accountId": "00000000-0000-0000-0000-000000000000" }
    ],
    "text": "Hello from letmepost.dev"
  }
  EOF
  ```

  ```ts TypeScript theme={"system"}
  const res = await fetch("https://api.letmepost.dev/v1/posts", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.LMP_KEY}`,
      "Idempotency-Key": crypto.randomUUID(),
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      targets: [
        { accountId: "00000000-0000-0000-0000-000000000000" },
      ],
      text: "Hello from letmepost.dev",
    }),
  });

  if (!res.ok) {
    const { error } = await res.json();
    throw new Error(`${error.code}: ${error.message}`);
  }
  const batch = await res.json();
  console.log(batch.id, batch.results[0].uri);
  ```

  ```python Python theme={"system"}
  import os, uuid, requests

  res = requests.post(
      "https://api.letmepost.dev/v1/posts",
      headers={
          "Authorization": f"Bearer {os.environ['LMP_KEY']}",
          "Idempotency-Key": str(uuid.uuid4()),
          "Content-Type": "application/json",
      },
      json={
          "targets": [
              {"accountId": "00000000-0000-0000-0000-000000000000"},
          ],
          "text": "Hello from letmepost.dev",
      },
  )
  res.raise_for_status()
  print(res.json()["results"][0]["uri"])
  ```

  ```go Go theme={"system"}
  package main

  import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
    "os"

    "github.com/google/uuid"
  )

  func main() {
    body, _ := json.Marshal(map[string]any{
      "targets": []map[string]string{
        {"accountId": "00000000-0000-0000-0000-000000000000"},
      },
      "text": "Hello from letmepost.dev",
    })
    req, _ := http.NewRequest("POST", "https://api.letmepost.dev/v1/posts", bytes.NewReader(body))
    req.Header.Set("Authorization", "Bearer "+os.Getenv("LMP_KEY"))
    req.Header.Set("Idempotency-Key", uuid.NewString())
    req.Header.Set("Content-Type", "application/json")

    res, err := http.DefaultClient.Do(req)
    if err != nil { panic(err) }
    defer res.Body.Close()
    fmt.Println(res.Status)
  }
  ```
</CodeGroup>

Replace the `accountId` with the id you copied from the dashboard. The active language tab is sticky — picking TypeScript here will follow you across every code sample on this site.

## 4. Read the response

A successful immediate publish returns `200 OK` with a batch envelope:

```json 200.json theme={"system"}
{
  "id": "batch_01HY6X4AWBJM2K9F2PTQMRD9JQ",
  "status": "published",
  "createdAt": "2026-05-16T12:00:00.000Z",
  "results": [
    {
      "accountId": "00000000-0000-0000-0000-000000000000",
      "platform": "twitter",
      "postId": "post_01HY6X4...",
      "status": "published",
      "uri": "https://x.com/yourhandle/status/1788..."
    }
  ]
}
```

The `results[].uri` is the public URL of the tweet. Save `id` (the batch id) and `results[].postId` — that's how you fetch the record later via `GET /v1/posts/:id`.

## 5. Fan out to multiple platforms

Same call, more `targets`. One request, N publishes, atomic shape preflight, structured per-target results:

```bash theme={"system"}
curl -X POST https://api.letmepost.dev/v1/posts \
  -H "Authorization: Bearer $LMP_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  --data @- <<EOF
{
  "targets": [
    { "accountId": "<twitter_account_id>" },
    { "accountId": "<linkedin_account_id>" },
    { "accountId": "<bluesky_account_id>" }
  ],
  "text": "Hello from three platforms at once"
}
EOF
```

The batch `status` summarizes the outcomes: `published` when every target succeeded, `partial_failed` on mixed outcomes, `failed` when none did. Each `results[]` entry tells you what happened on that platform.

## 6. What happens when it fails

Send a 400-character tweet and you'll get this back instead:

```json 400.json theme={"system"}
{
  "error": {
    "code": "preflight_failed",
    "rule": "twitter.text.max_graphemes",
    "platform": "twitter",
    "message": "Post text is 400 graphemes; Twitter (free tier) allows at most 280.",
    "remediation": "Shorten the post to 280 graphemes, or switch to a Premium-tier account (25,000 cap).",
    "docUrl": "https://docs.letmepost.dev/errors/preflight_failed",
    "ruleUrl": "https://docs.letmepost.dev/preflight/twitter-text-max_graphemes",
    "requestId": "req_01HY6X4AWBJM2K9F2PTQMRD9JQ"
  }
}
```

Every error carries the same envelope: stable `code`, the `rule` that fired, a `remediation`, a `requestId` you can grep, and `docUrl` / `ruleUrl` links straight to the right docs page. See [errors](/errors) for the full catalog and [error best practices](/errors#best-practices) for the contract.

## Next

<CardGroup cols={2}>
  <Card title="Platforms" icon="layer-group" href="/platforms-overview">
    Twitter, Instagram, LinkedIn, Facebook, Threads, Pinterest, Bluesky — caps, scope sets, and per-platform wisdom.
  </Card>

  <Card title="Idempotency" icon="repeat" href="/idempotency">
    Why every retry is safe.
  </Card>

  <Card title="Errors" icon="circle-exclamation" href="/errors">
    The twelve codes and the rule catalog.
  </Card>

  <Card title="API reference" icon="brackets-curly" href="/api-reference">
    `POST /v1/posts` parameter by parameter.
  </Card>
</CardGroup>
