Govir API

Read your posts and analytics, and draft, schedule and publish Threads posts from your own tools.

Last updated

The Govir API lets your own scripts and tools work with your Govir workspace: list your accounts and posts, read analytics, and create, schedule and publish posts. It uses the same rules as the app, so plan limits and posting safeguards apply to every request.

By using the API you agree to the API Terms.

Plans

PlanAccessKeys
FreeNo API—
Creatorread2
Proread, write, publish5
Agencyread, write, publish, with 3× the rate limits20

Authentication

Create a key in Settings → API. Only the workspace owner can manage keys. A key works in the space it was made in: on Agency, each client space has keys of its own. A key is shown once: store it like a password. Revoke it in the same place.

Send the key in the Authorization header of every request:

curl https://govir.app/api/v1/me \
  -H "Authorization: Bearer gvr_live_XXXXXXXX_YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY"

A key in the URL is refused (400), so it doesn't end up in logs.

Scopes

  • read: accounts, posts and analytics.
  • write: drafts, media, scheduling and unscheduling.
  • publish: publishing right away. It also needs Allow publishing right away to be on in Settings → API. Without it, a key can only save drafts and schedule posts at least 10 minutes ahead.

If your plan changes, a key keeps only the scopes the new plan includes.

Basics

  • Base URL: https://govir.app/api/v1
  • Requests and responses are JSON. Field names are snake_case.
  • Times are ISO 8601. Responses use UTC (2026-10-06T06:00:00.000Z); send a time with its offset (2026-10-06T09:00:00+03:00). Analytics ranges are days (YYYY-MM-DD) in the workspace's timezone.
  • Lists are paged with a cursor: pass next_cursor from one page as cursor for the next. limit is 1–100 (default 25).

Errors

Every error has the same shape:

{
  "error": {
    "code": "invalid_input",
    "message": "The request isn't valid",
    "details": { "issues": [{ "path": "parts.0.text", "message": "Too long" }] }
  }
}
StatusCodeWhen
400invalid_inputThe request isn't valid; details.issues says what
401unauthenticatedNo key, a wrong key, or a revoked one
402plan_limit, post_limitYour plan doesn't include this, or its monthly posts are used up
403forbiddenThe key lacks the scope (details.reason: "missing_scope"), or publishing right away isn't allowed (publish_scope, publish_disabled)
404not_foundNo such post, account or media in your workspace
409invalid_stateThe post can't do that now (for example, it's being published). In a client space that approves posts, scheduling a post the client hasn't approved gives details.reason: "needs_approval": send it for approval in the app
409needs_confirmationPosting safeguards found something to confirm (see Scheduling)
409idempotency_in_progressA request with the same Idempotency-Key is still running
422idempotency_mismatchThe Idempotency-Key was used with a different request
429rate_limitedToo many requests; wait Retry-After seconds

Rate limits

  • 60 requests a minute per key and 120 per workspace (Agency: 3×).
  • 5,000 requests a day per key (Agency: 3×).
  • 50 scheduling requests (schedule, queue, publish) an hour per key.

Every successful response has X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds) for the key's per-minute limit. A 429 has Retry-After.

Idempotency

Send an Idempotency-Key header (up to 255 visible characters, for example a UUID) with any POST. If you retry the same request with the same key within 24 hours, you get the first answer back with Idempotent-Replayed: true instead of creating a second post. Only successful answers are kept: after an error, fix the request and send it again with the same key.

Endpoints

Workspace

GET /me (read): the key's workspace, plan and scopes, and whether publishing right away is allowed. A good first call to check a key.

{
  "workspace": { "id": "…", "plan": "pro" },
  "key": { "prefix": "gvr_live_XXXXXXXX", "scopes": ["read", "write"] },
  "publish_enabled": false
}

Accounts

GET /accounts (read): the connected accounts.

{
  "data": [
    {
      "id": "…",
      "platform": "threads",
      "username": "yourname",
      "display_name": "Your Name",
      "avatar_url": "https://…",
      "status": "active"
    }
  ]
}

Posts

GET /posts (read): posts, newest first. Filters: status (draft, scheduled, publishing, published, failed, …), account_id, from and to (ISO times), limit, cursor.

GET /posts/{id} (read): one post with its parts and media.

{
  "id": "…",
  "status": "scheduled",
  "account_id": "…",
  "group_id": "…",
  "scheduled_at": "2026-10-06T06:00:00.000Z",
  "published_at": null,
  "permalink": null,
  "topic": "Writing",
  "reply_control": null,
  "part_numbers": "off",
  "quote": null,
  "error_code": null,
  "parts": [
    {
      "position": 0,
      "text": "Hello from the Govir API",
      "link_attachment": null,
      "poll_options": null,
      "gif": null,
      "text_attachment": null,
      "published": false,
      "media": []
    }
  ]
}

A post sent to several accounts is one post per account, sharing a group_id. Scheduling, unscheduling and publishing act on the whole group.

POST /posts (write): a new draft. Send text for a single post, or parts for a thread or a post with media.

curl https://govir.app/api/v1/posts \
  -H "Authorization: Bearer $GOVIR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2a7e-0d6b-4c55-9c1e-2b1f0e3b8a10" \
  -d '{
    "account_ids": ["…"],
    "parts": [
      { "text": "Three things I learned this week 🧵", "media": [{ "id": "…", "alt_text": "A chart" }] },
      { "text": "1. Write every day." }
    ],
    "topic": "Writing"
  }'

Fields: account_ids (required), text or parts (each part: text, media with id and alt_text, link_attachment, poll_options, gif with a GIPHY id, text_attachment with text up to 10,000 characters and an optional link_url), topic, reply_control (everyone, accounts_you_follow, mentioned_only, parent_post_author_only, followers_only), part_numbers (off, start or end: numbers like 1/3 on a thread's parts), quote_post_id (the id of one of your published posts to quote; the first part then needs its own text). The answer (201) is { "group_id": "…", "posts": [ … ] }.

PATCH /posts/{id} (write): change a draft. Fields you leave out keep their values.

DELETE /posts/{id} (write): delete a draft or a scheduled post. Published posts can't be deleted through the API.

Scheduling

POST /posts/{id}/schedule (write): schedule the post at a time, or in the next free slot of your queue.

{ "at": "2026-10-06T09:00:00+03:00" }
{ "queue": true }

Govir's posting safeguards check the time and text first: posts of one account too close together, or text much like another post. If they find something, nothing is scheduled and you get 409 needs_confirmation:

{
  "error": {
    "code": "needs_confirmation",
    "message": "Check the warnings, then send again with acknowledge_warnings: true",
    "details": {
      "warnings": [{ "code": "too_close", "account_id": "…", "suggested_at": "…" }],
      "suggested_at": "2026-10-06T07:00:00.000Z"
    }
  }
}

Send the same request with "acknowledge_warnings": true to go ahead, or pick another time (suggested_at is the nearest free one). A time less than 10 minutes away counts as publishing right away.

POST /posts/{id}/unschedule (write): back to drafts.

POST /posts/{id}/publish (publish): publish now. Needs the publish scope and Allow publishing right away. Takes acknowledge_warnings too.

Media

Uploading a file takes three steps:

  1. POST /media (write) with { "mime": "image/jpeg", "size": 123456 }. The answer has id and upload with method, url and headers.
  2. Upload the file: PUT its bytes to upload.url with exactly upload.headers. The URL works for 10 minutes.
  3. POST /media/{id}/complete (write). Images are ready at once; videos are processed, so follow them with GET /media/{id} until status is ready.

Then use the id in a post's parts[].media. Images: JPEG, PNG or WebP. Videos: MP4 or MOV. Files not used in a post within 24 hours are deleted.

GIFs

GET /gifs (write): GIPHY GIFs to attach, trending or a search with q (up to 50 characters). Page with offset from the last answer's next_offset.

{
  "data": [
    { "id": "3o7TKSjRrfIPjeiVyM", "title": "Wave", "width": 200, "height": 150, "url": "https://media.giphy.com/media/3o7TKSjRrfIPjeiVyM/giphy.webp", "preview_url": "https://media.giphy.com/media/3o7TKSjRrfIPjeiVyM/200w.webp" }
  ],
  "next_offset": 24,
  "attribution": "Powered by GIPHY"
}

Attach one with "gif": { "id": "…" } on a part. Threads takes GIFs from GIPHY only, on a part without images, videos or a poll; the GIF takes the link card's place. Show "Powered by GIPHY" where you show the results.

Analytics

GET /accounts/{id}/metrics (read): an account's overview for a period: posts, views, likes, replies, reposts and quotes, interactions, engagement rate and link clicks, the previous period of the same length, and per-day series. Views, likes, replies, reposts and quotes are the account's own, added up as Threads' Insights does for the same dates: every Threads day (Pacific Time) the dates touch in your timezone, listed in totals_range; post_views and shares are what the account's posts gained in the period; profile_views is the same number as views, kept for integrations made before October 2026. Query: from and to (YYYY-MM-DD), by default the last 28 days as Threads' Insights names them: up to yesterday by UTC, never after yesterday in your timezone. History follows your plan.

GET /accounts/{id}/metrics/posts (read): the account's posts by what they gained in the period. Query: from, to, sort (views by default, likes, replies, reposts, engagement_rate, published_at), limit. Each post has period (gained in the period) and lifetime totals.

GET /posts/{id}/metrics (read): a published post's totals and how they grew (history, with hours since publishing).

MCP

Govir is also an MCP server (setup for everyone: Connect Govir to your AI assistant), so AI assistants that support the Model Context Protocol can read your posts and analytics and draft and schedule posts for you, with the same scopes and limits as the API.

  • URL: https://govir.app/mcp (Streamable HTTP)
  • Sign-in: OAuth (the assistant opens Govir and you approve it), or an API key in the Authorization: Bearer <key> header

Claude (claude.ai and the Claude apps): in Settings → Connectors, choose Add custom connector, name it Govir and enter https://govir.app/mcp. When you connect it, Govir opens: check the access and choose Allow. Only the workspace owner can approve, and the plan's access applies.

Claude Code, with OAuth:

claude mcp add --transport http govir https://govir.app/mcp

Then run /mcp in Claude Code and choose Authenticate. Or with an API key:

claude mcp add --transport http govir https://govir.app/mcp \
  --header "Authorization: Bearer gvr_live_XXXXXXXX_YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY"

Other clients: add a remote MCP server with the URL above. Clients that support MCP authorization find Govir's OAuth endpoints on their own (/.well-known/oauth-protected-resource/mcp); others can send an API key in the Authorization header.

Apps you approve are listed in Settings → AI assistants (MCP); Disconnect ends their access at once. Access lasts an hour and renews by itself while the app is connected.

Tools, each shown only when the key's or the app's access allows it:

ToolScopeWhat it does
get_workspacereadPlan, scopes, timezone and the local time now
list_accountsreadConnected accounts
list_posts, get_postreadPosts and one post
get_account_metrics, get_top_posts, get_post_metricsreadAnalytics
create_draft, update_draftwriteDrafts (account_ids may be left out when there's one account)
search_gifswriteGIPHY GIFs to put on a part (gif: { id })
schedule_postwriteSchedule at a local time in your timezone (2026-10-07T09:00) or in the queue
unschedule_post, delete_postwriteBack to drafts, or delete a draft or scheduled post
publish_nowpublishPublish right away, when allowed in Settings → API

When Govir's posting safeguards find something to confirm, schedule_post says so instead of scheduling; the assistant should show you the warnings and go ahead only if you agree. Use a key with only the scopes the assistant needs: a write key can't publish right away.

OpenAPI

The machine-readable description is at /api/v1/openapi.json.