Surgemesh
Agent-first social publishing: connect channels, quote, and schedule posts across networks. Remote streamable-HTTP MCP server; free API key via POST https://api.surgemesh.com/v1/signup. By Novus Index LLC.
Hosted MCP Server
npx add-mcp 'https://api.surgemesh.com/mcp'Installs into Claude Code, Codex, Cursor and more
Documentation
Surgemesh API documentation
The Surgemesh API is a REST and MCP interface at https://api.surgemesh.com for scheduling, publishing, listing and cancelling social posts on Facebook Pages, Instagram and Bluesky (more networks listed in GET /v1/networks). Authenticate with "Authorization: Bearer sm_...", send JSON, and send an Idempotency-Key header when you create a post.
Base URL and authentication
- Base URL:
https://api.surgemesh.com - Auth header:
Authorization: Bearer sm_...(the key issm_followed by 40 characters). - Bodies: JSON. Money in responses is a JSON number in USD, for example
0.05. - Idempotency:
POST /v1/postsrequires anIdempotency-Keyheader (or anidempotency_keybody field). Other writes do not. - Errors:
application/problem+jsonwitherrorandhintfields. - Spec:
https://api.surgemesh.com/openapi.json.
Public endpoints with no key are GET /v1/pricing, GET /v1/quote and POST /v1/signup.
Quickstart in 4 steps
1. Sign up and get a sandbox key
curl -X POST https://api.surgemesh.com/v1/signup \
-H "Content-Type: application/json" \
-d '{"email": "dev@example.com", "accept_terms": true, "name": "Harbor Cafe", "brand": "Harbor Cafe"}'
{
"tenant_id": "t_k3m9q2x7p4w8",
"plan": "sandbox",
"brand_id": "b_r7t2kq",
"api_key": "sm_...",
"key_id": "<uuid>",
"verification": "unverified",
"note": "Key shown once. Sandbox works now: it validates and prices posts but never publishes. To go live, verify ownership once (GET /v1/onboarding lists the steps), then add credit.",
"next": [{"method": "GET", "href": "/v1/onboarding"}, {"method": "POST", "href": "/v1/channels"}, {"method": "GET", "href": "/v1/quote"}, {"method": "POST", "href": "/v1/posts"}]
}
The signup key has scopes read, write and admin and access to all brands (["*"]). name and brand are optional. accept_terms: true is required and accepts the Terms and Privacy Policy.
2. Connect a channel
curl -X POST https://api.surgemesh.com/v1/channels \
-H "Authorization: Bearer sm_..." \
-H "Content-Type: application/json" \
-d '{
"brand_id": "b_r7t2kq",
"network": "instagram",
"label": "Harbor IG",
"credentials": { "token": "<Meta access token>", "ig_user_id": "17841400000000000" }
}'
{
"id": "ch_k2m9p4x7q3r8",
"brand_id": "b_r7t2kq",
"network": "instagram",
"connected": true,
"note": "Credentials are encrypted at rest and never returned."
}
For a Facebook Page, use "network": "facebook" and page_id instead of ig_user_id.
3. Get a quote
curl "https://api.surgemesh.com/v1/quote?scheduled_at=2026-11-03T14:00:00Z&deliveries=2"
{
"scheduled_at": "2026-11-03T14:00:00.000Z",
"deliveries": 2,
"price_usd": <exact price for this time>,
"price_micros": <the same price in millionths of a dollar>,
"cheaper_times": [
{ "scheduled_at": "<a nearby time that costs less>", "price_usd": <its exact price> }
]
}
4. Create the post at the cheaper time
curl -X POST https://api.surgemesh.com/v1/posts \
-H "Authorization: Bearer sm_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 0d4b7f1e-5a2c-4e8b-b3f9-7c6a5d4e3f21" \
-d '{
"brand_id": "b_r7t2kq",
"channels": ["ch_fb...", "ch_ig..."],
"body": "Spring menu is live.",
"media_urls": ["https://cdn.example.com/spring.jpg"],
"scheduled_at": "2026-11-03T14:30:00Z",
"max_price_usd": 0.10
}'
{
"id": "<uuid>",
"status": "accepted",
"sandbox": false,
"scheduled_at": "2026-11-03T14:30:00.000Z",
"price_usd": <the locked price>,
"charged_usd": <the same amount>,
"cancelled": false
}
The response is 201. From a sandbox account (or to a sandbox test channel) the same request returns "sandbox": true with the same price_usd and "charged_usd": 0: it is validated exactly like a live post, never published and never charged. All timestamps are ISO 8601 UTC with Z.
Post rules
body: 1 to 5,000 characters, and within each target network's own limit (Bluesky 300 graphemes, Instagram 2,200 characters with at most 30 hashtags and 20 mentions). Sandbox and live check the same limits;GET /v1/networkslists them.media_urls: up to 10, public HTTPS only, within each network's count, type and size limits (each URL is checked with HEAD). Instagram needs at least one image (JPEG) or video.channels: 1 to 20 channel ids, all belonging tobrand_id.scheduled_at: ISO 8601 (e.g.2026-11-03T14:30:00Z), in the future (5 minutes of grace) and at most 366 days ahead.max_price_usd: optional. If the locked price would exceed it, the API returns409 price_above_maxwithcheaper_times.- Charging: the locked price is debited from prepaid credit when the post is accepted. Failed deliveries are refunded to credit automatically.
List posts with GET /v1/posts?brand_id=b_r7t2kq. Each post has a status (pending while awaiting review, accepted, rejected or cancelled), boolean cancelled and sandbox, price_usd and charged_usd, and each target a state: queued, scheduled, processing, published, failed, cancelled or sandboxed.
MCP client configuration
The MCP server speaks JSON-RPC 2.0 at POST https://api.surgemesh.com/mcp, with JSON responses (no SSE). It supports initialize (protocol version 2025-06-18), tools/list, tools/call and ping, and authenticates with the same Bearer header.
{
"mcpServers": {
"surgemesh": {
"url": "https://api.surgemesh.com/mcp",
"headers": { "Authorization": "Bearer sm_..." }
}
}
}
| Tool | Arguments | REST equivalent |
|---|---|---|
get_pricing | none | GET /v1/pricing |
get_quote | scheduled_at, deliveries | GET /v1/quote |
list_channels | none | GET /v1/channels |
list_brands | none | GET /v1/brands |
create_post | idempotency_key, brand_id, body, media_urls, scheduled_at, channels, max_price_usd | POST /v1/posts |
list_posts | brand_id | GET /v1/posts |
cancel_post | id | DELETE /v1/posts/{id} |
get_usage | none | GET /v1/usage |
get_onboarding | none | GET /v1/onboarding |
start_email_verification / confirm_email_verification | verification_id, code | POST /v1/verify/email, /confirm |
start_dns_verification / check_dns_verification | domain, verification_id | POST /v1/verify/dns, /check |
request_owner_approval / get_verification | note, callback_url, verification_id | POST /v1/verify/approval, GET /v1/verify/{id} |
rotate_my_key | grace_seconds | POST /v1/keys/self/rotate |
get_analytics | from, to, granularity, brand_id | GET /v1/analytics |
In each tool result, content[0].text holds the REST JSON. On errors, isError is true and the JSON includes error and hint.
Endpoints
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /v1/pricing | Public | Base price, time-of-day pricing, emergency pledge, packs, payment status |
| GET | /v1/quote | Public | Price for scheduled_at and deliveries |
| POST | /v1/signup | Public | Create a sandbox tenant, first brand and API key |
| GET | /v1/me | Key | Plan, status, credits, key scopes, settings |
| POST | /v1/me/settings | admin | Account settings: {"trend_opt_out": true} opts out of any future aggregated trend statistics (see Terms) |
| GET | /v1/usage | Key | Monthly deliveries and the credit ledger |
| GET | /v1/brands | Key | List brands |
| POST | /v1/brands | admin | Create a brand (max 25) |
| GET | /v1/channels | Key | List channels |
| GET | /v1/networks | None | Every network: status, how to connect, review limits, media limits |
| POST | /v1/channels | admin | Connect a Facebook Page, Instagram account or Bluesky account with credentials |
| POST | /v1/channels/connect | admin | Start a one-tap connect; returns connect_url for the account owner |
| GET | /v1/channels/connect/{id} | Key | Connect status: pending, connected (with channel_id), failed, expired |
| DELETE | /v1/channels/{id} | admin | Disconnect a channel and erase its credentials |
| GET | /v1/posts | Key | List posts for a brand |
| POST | /v1/posts | write | Create a post (Idempotency-Key required) |
| DELETE | /v1/posts/{id} | write | Cancel queued deliveries and refund them |
| GET / POST | /v1/keys | admin | List or create API keys (max 50 active) |
| DELETE | /v1/keys/{id} | admin | Revoke a key |
| POST | /v1/keys/{id}/rotate | Key / admin | Rotate a key (self = the calling key); the old key works through grace_seconds |
| GET | /v1/onboarding | Key | Remaining steps to go live and the next call |
| POST | /v1/verify/email, /v1/verify/email/confirm | admin | Email a 6-digit code, then submit it |
| POST | /v1/verify/dns, /v1/verify/dns/check | admin | Get a TXT record for your domain, then check it |
| POST | /v1/verify/approval | admin | Email the owner a one-tap Approve link |
| GET | /v1/verify/{id} | Key | Verification status (poll this) |
| GET | /v1/analytics | Key | Deliveries, failures, spend and pricing over time, per brand |
| POST | /v1/billing/topup | admin | Buy a credit pack via x402 (not enabled in beta) |
| POST | /mcp | Key | MCP JSON-RPC endpoint |
Brands, channels and keys
A brand groups the channels of one business or client. A channel is one Facebook Page or one Instagram account. Create a brand with POST /v1/brands and {"name": "Harbor Cafe Catering"}, which returns 201 with {"id": "b_x8p3mn", "name": "Harbor Cafe Catering"}.
Mint a narrower key for each agent or client:
curl -X POST https://api.surgemesh.com/v1/keys \
-H "Authorization: Bearer sm_..." \
-H "Content-Type: application/json" \
-d '{"name": "client-a-agent", "scopes": ["read", "write"], "brands": ["b_r7t2kq"], "expires_at": "2027-01-01T00:00:00Z"}'
{
"id": "<uuid>",
"key": "sm_...",
"scopes": ["read", "write"],
"brands": ["b_r7t2kq"],
"note": "Shown once. Store it in a secret manager."
}
A brand-restricted key cannot mint a "*" (all-brands) key. GET /v1/keys lists id, name, prefix, scopes, brands, created_at, last_used_at, expires_at and revoked, never the full key. DELETE /v1/keys/{id} returns {"revoked": "<uuid>"}.
Idempotency
POST /v1/posts requires an Idempotency-Key header, or an idempotency_key field in the body. Without it, the API returns 400 idempotency_key_required.
- Same key, same body: the original result with
"idempotent_replay": trueand HTTP200. No second post, no second charge. - Same key, different body:
409 idempotency_key_reused.
Other writes do not need a key. DELETE requests are naturally idempotent, and a top-up is protected because each on-chain payment can credit only once.
Errors
Errors use application/problem+json with a stable error code and a hint describing the next step. Validation errors add field:
{
"type": "https://surgemesh.com/errors/invalid-field",
"title": "Invalid field",
"status": 400,
"detail": "Invalid field: scheduled_at",
"error": "invalid_field",
"field": "scheduled_at",
"hint": "Fix the field named in \`field\` and retry. Schemas: GET /openapi.json."
}
| Status | error codes | What to do |
|---|---|---|
| 400 | invalid_field, invalid_json, idempotency_key_required, invalid_payment_header | Fix the request (see field) and resend |
| 401 | unauthorized, key_expired | Send a valid, unexpired Bearer key |
| 402 | insufficient_credit, payment_required, payment_invalid, payment_not_settled | Top up or fix the x402 payment, then retry |
| 403 | account_suspended, insufficient_scope, brand_not_allowed | Use a key with the right scope or brand; do not retry unchanged |
| 404 | brand_not_found, channel_not_found, not_found | Check the id |
| 409 | idempotency_key_reused, price_above_max, post_not_cancelable, email_taken | Read hint; requote, change the time or read post state |
| 413 | payload_too_large | Send a smaller body |
| 415 | json_required | Send Content-Type: application/json |
| 422 | invalid_media_url | Use a public HTTPS URL |
| 429 | rate_limited, sandbox_limit, signup_limited | For rate_limited, wait Retry-After seconds |
| 500 | internal_error | Retry with backoff; quote request_id if reporting |
| 503 | payments_not_enabled | Payments are off during the beta; keep using the sandbox |
Rate limits
Requests, signups and verification sends are rate limited. Exceeding a limit returns 429 rate_limited (or signup_limited) with a Retry-After header: wait that many seconds, then retry with the same Idempotency-Key.
Top up credits (MPP card or stablecoin, or x402)
Payments are not enabled during the beta. Today, POST /v1/billing/topup returns 503 payments_not_enabled. When payments are enabled, the flow is:
- Call
POST /v1/billing/topupwith{"pack": "pack_20"}("20"is also accepted) using an admin key. - Without an
X-PAYMENTheader, Surgemesh answers402with x402 requirements inaccepts. - Your x402 client signs a USDC payment on Base matching the requirement and sends it, base64-encoded, in
X-PAYMENT. - Surgemesh verifies the payment and answers
200with anX-PAYMENT-RESPONSEheader. A payment that needs review returns202 payment_under_review. - Every 402 carries
terms_urlandterms_version. Sending payment means the operator behind the agent accepts the Terms.
{
"x402Version": 1,
"error": "payment_required",
"accepts": [{
"scheme": "exact",
"network": "base",
"maxAmountRequired": "20000000",
"resource": "https://api.surgemesh.com/v1/billing/topup#pack_20",
"description": "Surgemesh credit pack_20: pay $20, get $20 credit",
"mimeType": "application/json",
"payTo": "0x...",
"maxTimeoutSeconds": 300,
"asset": "<USDC contract on Base>",
"extra": { "name": "USD Coin", "version": "2" }
}],
"hint": "..."
}
{ "credited_usd": 20, "credit_usd": 20, "plan": "live", "transaction": "0x..." }
A settled transaction can credit only once. Top-ups need a verified owner (403 verification_required otherwise; see GET /v1/onboarding). The first top-up switches the account from sandbox to live.
MPP rails (coming soon, off during the beta). Send {"pack": "pack_20", "rail": "mpp_card"} (or "mpp_stablecoin"). The 402 carries a WWW-Authenticate: Payment challenge (Machine Payments Protocol). For a card, get a one-time Shared Payment Token for the network_id in the 402 body (for example from the Link agent wallet) for the exact amount; for a stablecoin, pay the Tempo or Solana challenge. Retry the same request with the credential in Payment-Authorization (your Bearer key stays in Authorization). Success returns 200 with a Payment-Receipt header; a payment that needs review returns 202 payment_under_review. Each payment credits once. A refund or chargeback removes the credits it bought, and top-ups pause while a dispute is open. MCP: get_topup_link.
Card checkout for a person (coming soon). POST /v1/billing/checkout with {"pack": "pack_20"} (admin key, verified owner, optional Idempotency-Key) returns 201 with a Stripe-hosted checkout_url and session_id. Give the link to the person paying; credits are added only when Stripe confirms the payment, then show in GET /v1/me. Off during the beta: 503 checkout_not_enabled. MCP: create_checkout_link.
Referral credits (coming soon). GET /v1/referrals (verified owners) returns your code, share link and stats. New accounts pass "ref": "<code>" to POST /v1/signup; the first referral recorded is the one that counts. GET /v1/referrals/lookup?code= checks a code (no key, rate limited). Rewards are credits only, never cash. MCP: get_referral_code.