Hapi API documentation
One REST API to connect social accounts, publish and schedule posts, and read analytics across 16 platforms.
Running locally? Use http://localhost:3100/v1.
Getting started
- Start a 15-day trial. Your workspace comes with labelled demo data and sandbox accounts so you can call the API right away.
- Create an API key in Dashboard → API keys. The secret is shown once — store it in an environment variable.
- Connect social accounts in Dashboard → Social accounts (OAuth, credentials, or a sandbox account).
- List your accounts, then publish with one request.
curl -X GET "https://hapi.tools/v1/accounts" \
-H "Authorization: Bearer hapi_live_xxxxxxxxxxxxxxxx"Authentication
Authenticate every request with a bearer token. Keys look like hapi_live_… and belong to one organization.
curl https://hapi.tools/v1/accounts \
-H "Authorization: Bearer hapi_live_xxxxxxxx"- Scopes.
READkeys can only call GET endpoints.READ_WRITEkeys can create, update and delete. - Profile restrictions. A key can be limited to specific profiles; it then only sees accounts and posts within them.
- Storage. We store a peppered SHA-256 hash of each key — never the key itself. Lost keys can't be recovered, only revoked and replaced.
- Never call the API from a browser or mobile app with a secret key. Proxy requests through your backend.
Sandbox mode
Sandbox accounts let you build and test without platform credentials. They run every request through the same validation as live accounts (character limits, media rules, required options), but never contact the platform.
- Sandbox targets finish with status
sandbox_publishedand"sandbox": true. They are never reported as real publishes. - Include
[sandbox:fail]in the content to force a failure and test your error handling. - Sandbox accounts count toward your plan's account limit and can be removed at any time.
Publishing posts
A post has one piece of content and one or more targets — the social accounts it goes to. Pass accountIds, platforms, or both (platforms then filters the accounts). Each target is validated against its platform's real limits before anything is published.
- Omit
scheduledAt(or sendnull) to publish immediately — the response contains the final per-target result. - Override text or supply required fields per platform with
platformOptions, e.g.{"reddit": {"subreddit": "webdev", "title": "…"}}. - Attach media by
mediaIds(from POST /v1/media) or publicmediaUrls. - Send an
Idempotency-Keyheader to make retries safe — the same key returns the original post. firstCommentis posted as a comment or self-reply on platforms that support it.
curl -X POST "https://hapi.tools/v1/posts" \
-H "Authorization: Bearer hapi_live_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"accountIds": [
"account_123"
],
"content": "Our new product is live!",
"platforms": [
"instagram",
"linkedin"
],
"mediaUrls": [
"https://example.com/image.jpg"
],
"scheduledAt": null
}'Scheduling
Set scheduledAt to an ISO-8601 timestamp with an offset (for example 2026-11-20T09:00:00-05:00). The post is stored with status scheduled and published by Hapi's scheduler within about 15 seconds of that time. Pass timezone (IANA name) to record the author's intent for display.
Scheduled posts can be edited or rescheduled with PATCH /v1/posts/:id until they start publishing. Drafts ("draft": true) are never published automatically.
curl -X POST "https://hapi.tools/v1/posts" \
-H "Authorization: Bearer hapi_live_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"platforms": [
"linkedin",
"x"
],
"content": "Webinar starts in one hour 👋",
"scheduledAt": "2026-11-20T09:00:00-05:00",
"timezone": "America/New_York"
}'Analytics
Analytics are normalized into impressions, reach, likes, comments, shares, clicks and followers. Values come from each platform's API where it exposes them; some (Slack, Discord webhooks, LinkedIn member profiles) expose none, and Bluesky exposes followers and per-post counts only. Demo data is flagged so it's never mistaken for live numbers.
curl -X GET "https://hapi.tools/v1/analytics?from=2026-10-01&to=2026-10-31&platform=x" \
-H "Authorization: Bearer hapi_live_xxxxxxxxxxxxxxxx"Webhooks
Register an HTTPS endpoint to receive events as they happen. Each delivery is a JSON POST with these headers:
hapi-eventstring | Event type, e.g. post.published. |
hapi-deliverystring | Unique delivery id — use it to de-duplicate. |
hapi-signaturestring | t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>" using your endpoint secret> |
| Event | When |
|---|---|
| post.scheduled | A post was accepted with a future scheduledAt. |
| post.published | Every target published (sandbox targets count, flagged with sandbox: true). |
| post.partial | Some targets published and some failed. |
| post.failed | No target could be published. |
| account.connected | A social account was connected or reconnected. |
| account.disconnected | A social account was removed. |
Respond with any 2xx within 10 seconds. Failed deliveries are retried with exponential backoff (30s, 1m, 2m, 4m…) for up to 6 attempts, and can be retried manually from the dashboard or the API. Verify the signature before trusting a payload:
import crypto from "node:crypto";
export function verify(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ""));
}
// Express: app.post("/hooks/hapi", express.raw({ type: "application/json" }), (req, res) => {
// if (!verify(req.body.toString(), req.get("hapi-signature"), process.env.HAPI_WEBHOOK_SECRET)) return res.sendStatus(400);
// res.sendStatus(200);
// });AI agents (MCP)
Hapi runs a remote Model Context Protocol server at https://hapi.tools/mcp. Any MCP-capable agent (Claude, Codex, Gemini CLI, Cursor, VS Code Copilot, Windsurf) can publish, schedule and analyze with no code: add the server with your API key and ask in plain language. The fastest path is Dashboard → AI agents, which creates a key and fills it into these snippets.
claude mcp add --transport http hapi https://hapi.tools/mcp \
--header "Authorization: Bearer YOUR_HAPI_API_KEY"Then run /mcp inside Claude Code to confirm it's connected.
Transport is stateless Streamable HTTP; authenticate with the same Authorization: Bearer key as the REST API. Rate limits, plan limits, profile restrictions and the audit log all apply. Write tools need a READ_WRITE key, so give reporting agents a READ key.
| Tool | What it does | Key |
|---|---|---|
| get_workspace | Plan, limits, usage this month and whether the key can write | read |
| list_platforms | Every platform's limits, features and required options | read |
| list_accounts | Connected accounts, including which are sandbox | read |
| create_post | Publish now, schedule or draft across accounts in one call | read-write |
| list_posts | Posts filtered by status, platform or date | read |
| get_post | Per-platform result, live URL or error for one post | read |
| update_post | Edit a draft or scheduled post | read-write |
| delete_post | Delete or cancel a post | read-write |
| retry_post | Retry only the targets that failed | read-write |
| get_analytics | Daily followers, impressions, engagement and clicks | read |
No MCP support? Point the agent at /llms.txt, a plain-text guide to the REST API written for LLMs, or define your own tools over the endpoints below. ChatGPT and claude.ai web connectors need OAuth sign-in, which is coming soon.
API reference
All endpoints accept and return JSON (except media upload, which takes multipart/form-data). Timestamps are ISO-8601 in UTC. List endpoints paginate with page and limit.
List accounts
Returns the social accounts this key can access. status is active, expired (reconnect needed), revoked or error. sandbox: true marks simulated accounts.
curl -X GET "https://hapi.tools/v1/accounts" \
-H "Authorization: Bearer hapi_live_xxxxxxxxxxxxxxxx"{
"data": [
{
"id": "account_123",
"platform": "linkedin",
"username": "acme-inc",
"displayName": "Acme Inc.",
"avatarUrl": null,
"status": "active",
"sandbox": false,
"profileId": "prof_1",
"connectedAt": "2026-10-02T10:00:00.000Z"
}
]
}Create post
contentstring | Post text. Validated per platform (e.g. 280 for X, 300 graphemes for Bluesky). |
accountIdsstring[] | Accounts to publish to. |
platformsstring[] | Publish to all active accounts on these platforms, or filter accountIds. |
mediaIdsstring[] | Media from POST /v1/media, in order. |
mediaUrlsstring[] | Public URLs to attach. |
scheduledAtstring | null | ISO-8601 with offset. Null/omitted publishes now. |
draftboolean | Save without publishing or validating. |
timezonestring | IANA timezone for display. |
firstCommentstring | Comment/reply added after publishing where supported. |
platformOptionsobject | Per-platform {content, title, subreddit, boardId}. |
idempotencyKeystring | Same as the Idempotency-Key header. |
Returns 201 with the post. When publishing immediately, status is the final outcome: published, partial or failed; inspect targets[].error for failures. Validation failures return 422 with one entry per problem.
curl -X POST "https://hapi.tools/v1/posts" \
-H "Authorization: Bearer hapi_live_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"accountIds": [
"account_123"
],
"content": "Our new product is live!",
"platforms": [
"instagram",
"linkedin"
],
"mediaUrls": [
"https://example.com/image.jpg"
],
"scheduledAt": null
}'{
"id": "cmh2k8x0p0001",
"content": "Our new product is live!",
"status": "published",
"scheduledAt": null,
"publishedAt": "2026-11-01T14:02:11.000Z",
"timezone": "UTC",
"firstComment": null,
"source": "api",
"isDemo": false,
"media": [
{
"id": "cmh2k7q1z0000",
"url": "https://example.com/image.jpg",
"kind": "image"
}
],
"targets": [
{
"accountId": "account_123",
"platform": "linkedin",
"username": "acme-inc",
"status": "published",
"sandbox": false,
"platformPostId": "urn:li:share:7251…",
"url": "https://www.linkedin.com/feed/update/urn:li:share:7251…/",
"error": null,
"publishedAt": "2026-11-01T14:02:11.000Z"
},
{
"accountId": "account_456",
"platform": "instagram",
"username": "@instagram_sandbox",
"status": "sandbox_published",
"sandbox": true,
"platformPostId": "sbx_instagram_9fK2…",
"url": null,
"error": null,
"publishedAt": "2026-11-01T14:02:10.000Z"
}
],
"createdAt": "2026-11-01T14:02:09.000Z",
"updatedAt": "2026-11-01T14:02:11.000Z"
}List posts
statusstring | draft | scheduled | publishing | published | partial | failed |
from, todate | Filter by scheduled (or created) time. |
platformstring | Only posts targeting this platform. |
page, limitnumber | Pagination. limit max 100. |
curl -X GET "https://hapi.tools/v1/posts?status=scheduled&limit=20" \
-H "Authorization: Bearer hapi_live_xxxxxxxxxxxxxxxx"{
"data": [
"…post objects…"
],
"pagination": {
"page": 1,
"limit": 20,
"total": 42,
"hasMore": true
}
}Retrieve, update or delete a post
GET /v1/posts/{id} returns the post with per-target results.
PATCH /v1/posts/{id} accepts any create field. Only draft, scheduled and failed posts can be edited; non-draft updates are re-validated and need a future scheduledAt.
DELETE /v1/posts/{id} returns 204. It removes the post from Hapi; content already live on a platform stays there.
curl -X PATCH "https://hapi.tools/v1/posts/cmh2k8x0p0001" \
-H "Authorization: Bearer hapi_live_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"scheduledAt": "2026-11-21T15:00:00Z"
}'Retry a post
Re-publishes only the failed targets of a failed or partial post. Successful targets are never published twice.
curl -X POST "https://hapi.tools/v1/posts/cmh2k8x0p0001/retry" \
-H "Authorization: Bearer hapi_live_xxxxxxxxxxxxxxxx"Upload media
Send the file as multipart field file. Accepted: JPEG, PNG, WebP (8 MB), GIF (15 MB), MP4 and MOV (512 MB). Files are verified by content, not extension.
curl -X POST "https://hapi.tools/v1/media" \
-H "Authorization: Bearer hapi_live_xxxxxxxxxxxxxxxx" \
-F "file=@./launch.jpg"{
"id": "cmh2k7q1z0000",
"url": "https://hapi.tools/api/media/…/a1b2.jpg",
"kind": "image",
"mimeType": "image/jpeg",
"sizeBytes": 482113
}Get analytics
from, todate | Defaults to the last 30 days. |
platformstring | Only this platform. |
accountIdstring | Only this account. |
curl -X GET "https://hapi.tools/v1/analytics?from=2026-10-01&to=2026-10-31" \
-H "Authorization: Bearer hapi_live_xxxxxxxxxxxxxxxx"{
"data": {
"isDemo": false,
"totals": {
"impressions": 48210,
"reach": 34711,
"likes": 2169,
"comments": 289,
"shares": 193,
"clicks": 578,
"followers": 12840,
"engagement": 2651,
"engagementRate": 0.055
},
"series": [
"…daily rows…"
],
"platforms": [
"…per platform…"
]
}
}List platform capabilities
Machine-readable limits and capabilities for every platform, plus whether live connections are enabled in this deployment. Use it to drive your own composer UI.
curl -X GET "https://hapi.tools/v1/platforms" \
-H "Authorization: Bearer hapi_live_xxxxxxxxxxxxxxxx"Webhook endpoints
GET /v1/webhooks lists endpoints. POST creates one and returns its signing secret once. DELETE /v1/webhooks/{id} removes it. POST /v1/webhooks/deliveries/{id}/retry re-sends a delivery.
curl -X POST "https://hapi.tools/v1/webhooks" \
-H "Authorization: Bearer hapi_live_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/hapi",
"events": [
"post.published",
"post.failed"
]
}'{
"id": "wh_1",
"url": "https://example.com/hooks/hapi",
"events": [
"post.published",
"post.failed"
],
"enabled": true,
"secret": "whsec_…"
}Platform capabilities
Platforms don't all support the same things. Hapi enforces each platform's actual rules instead of pretending otherwise.
| Platform | Connect via | Access | Chars | Images | Text-only | Video | 1st comment | Analytics | Notes |
|---|---|---|---|---|---|---|---|---|---|
| OAuth 2.0 | App review | 2,200 | 10 | ○ | ● | ● | ● | Professional (Business or Creator) accounts only, via the Instagram Graph API. | |
| TikTok | OAuth 2.0 | App review | 2,200 | 35 | ○ | ● | ○ | ● | Content Posting API. Video or photo carousel required — no text-only posts. |
| X | OAuth 2.0 | Open | 280 | 4 | ● | ● | ● | ● | OAuth 2.0 with PKCE; posting uses POST /2/tweets. |
| OAuth 2.0 | Open | 3,000 | 20 | ● | ● | ● | ● | Members via "Share on LinkedIn" (w_member_social). Company pages require Community Management API approval. | |
| OAuth 2.0 | App review | 63,206 | 10 | ● | ● | ● | ● | Facebook Pages only — personal profiles cannot be published to via the API. | |
| Threads | OAuth 2.0 | App review | 500 | 20 | ● | ● | ● | ● | Threads API: 250 published posts per profile per 24 hours. |
| YouTube | OAuth 2.0 | App review | 5,000 | 0 | ○ | ● | ○ | ● | Video uploads only (including Shorts). Content becomes the description. |
| OAuth 2.0 | App review | 500 | 5 | ○ | ● | ○ | ● | Every Pin needs an image or video and a destination board. | |
| Bluesky | Credentials | Open | 300 | 4 | ● | ● | ● | ○ | Connects with an app password over the AT Protocol — no developer app needed. |
| OAuth 2.0 | Open | 40,000 | 1 | ● | ○ | ● | ○ | Each post targets one subreddit and needs a title. Subreddit rules still apply. | |
| Snapchat | OAuth 2.0 | Partner only | 160 | 1 | ○ | ● | ○ | ○ | Publishing to Public Profiles (Stories, Spotlight) requires Snap partner approval. Sandbox only until approved. |
| Discord | Credentials | Open | 2,000 | 10 | ● | ● | ○ | ○ | Posts to a channel through an incoming webhook. |
| Credentials | App review | 4,096 | 1 | ● | ● | ○ | ○ | WhatsApp is a messaging channel — the Cloud API sends messages to recipients, it has no public feed. | |
| Telegram | Credentials | Open | 4,096 | 10 | ● | ● | ○ | ○ | Bot API. Captions on media are limited to 1,024 characters. |
| Slack | Credentials | Open | 40,000 | 10 | ● | ○ | ○ | ○ | Posts through a Slack incoming webhook. Images are embedded by URL. |
| Google Business Profile | OAuth 2.0 | App review | 1,500 | 1 | ● | ○ | ○ | ● | Local posts (updates, offers, events). Requires Business Profile API access approval from Google. |
Rate limits
Limits apply per API key over a one-minute window and depend on your plan:
| Plan | Requests / minute | Accounts | Posts / month |
|---|---|---|---|
| Trial | 120 | 5 | 300 |
| Growth | 300 | 25 | 5,000 |
| Pro | 1,200 | 100 | 25,000 |
| Scale | 6,000 | Unlimited | Unlimited |
Every response includes X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds until the window resets). Over the limit you'll get 429 rate_limited with a Retry-After header. Platforms also enforce their own quotas (for example, Instagram's 100 API posts per 24 hours); those surface as target errors with code platform_rate_limited.
Error codes
Errors use one envelope. details is present for validation errors and lists each problem.
{
"error": {
"code": "validation_error",
"message": "Post failed platform validation",
"details": [
{
"accountId": "account_789",
"platform": "x",
"message": "X: 312/280 characters"
}
]
}
}| Code | HTTP | Meaning |
|---|---|---|
| bad_request | 400 | Malformed request, e.g. invalid JSON or a missing multipart file. |
| unauthorized | 401 | Missing, invalid, expired or revoked API key. |
| plan_limit | 402 | Your plan's account or monthly post limit was reached. |
| forbidden | 403 | Key is read-only, or the resource is outside the key's profiles. |
| not_found | 404 | The resource doesn't exist in your organization. |
| conflict | 409 | The resource is in a state that doesn't allow this action (e.g. editing a published post). |
| validation_error | 422 | Body failed schema or platform validation. `details` lists each issue. |
| rate_limited | 429 | Too many requests. Wait for `Retry-After` seconds. |
| internal_error | 500 | Unexpected server error. Safe to retry with an Idempotency-Key. |
Messaging API
BetaNot yet available in the public API. Contact sales@hapi.tools to join the beta.
Send and receive direct messages on platforms with messaging APIs (Instagram, Facebook Messenger, WhatsApp Cloud API).
Inbox API
BetaNot yet available in the public API. Contact sales@hapi.tools to join the beta.
A unified, normalized stream of comments, mentions and messages across accounts.
Ads API
BetaNot yet available in the public API. Contact sales@hapi.tools to join the beta.
Boost posts and read paid performance alongside organic analytics.
Security
OAuth tokens and platform credentials are encrypted at rest with AES-256-GCM and only decrypted server-side at publish time. API keys are stored as peppered hashes. Webhook payloads are HMAC-signed. All tenant data is isolated per organization and every privileged action is written to an audit log. Report vulnerabilities to support@hapi.tools.
Terms of service
Placeholder — the full terms of service will be published here before general availability. You remain responsible for complying with each social platform's developer terms.
Privacy policy
Placeholder — the full privacy policy will be published here. We only request the platform permissions needed to publish and read analytics, and you can disconnect accounts at any time.
Comments API
BetaNot yet available in the public API. Contact sales@hapi.tools to join the beta.
Read, reply to, hide and delete comments on published posts.