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
| Plan | Access | Keys |
|---|---|---|
| Free | No API | — |
| Creator | read | 2 |
| Pro | read, write, publish | 5 |
| Agency | read, write, publish, with 3× the rate limits | 20 |
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_cursorfrom one page ascursorfor the next.limitis 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" }] }
}
}
| Status | Code | When |
|---|---|---|
| 400 | invalid_input | The request isn't valid; details.issues says what |
| 401 | unauthenticated | No key, a wrong key, or a revoked one |
| 402 | plan_limit, post_limit | Your plan doesn't include this, or its monthly posts are used up |
| 403 | forbidden | The key lacks the scope (details.reason: "missing_scope"), or publishing right away isn't allowed (publish_scope, publish_disabled) |
| 404 | not_found | No such post, account or media in your workspace |
| 409 | invalid_state | The 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 |
| 409 | needs_confirmation | Posting safeguards found something to confirm (see Scheduling) |
| 409 | idempotency_in_progress | A request with the same Idempotency-Key is still running |
| 422 | idempotency_mismatch | The Idempotency-Key was used with a different request |
| 429 | rate_limited | Too 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:
POST /media(write) with{ "mime": "image/jpeg", "size": 123456 }. The answer hasidanduploadwithmethod,urlandheaders.- Upload the file:
PUTits bytes toupload.urlwith exactlyupload.headers. The URL works for 10 minutes. POST /media/{id}/complete(write). Images are ready at once; videos are processed, so follow them withGET /media/{id}untilstatusisready.
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:
| Tool | Scope | What it does |
|---|---|---|
get_workspace | read | Plan, scopes, timezone and the local time now |
list_accounts | read | Connected accounts |
list_posts, get_post | read | Posts and one post |
get_account_metrics, get_top_posts, get_post_metrics | read | Analytics |
create_draft, update_draft | write | Drafts (account_ids may be left out when there's one account) |
search_gifs | write | GIPHY GIFs to put on a part (gif: { id }) |
schedule_post | write | Schedule at a local time in your timezone (2026-10-07T09:00) or in the queue |
unschedule_post, delete_post | write | Back to drafts, or delete a draft or scheduled post |
publish_now | publish | Publish 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.