Pulse Publish
Pulse Publish API

Build with Pulse Publish.

Add video publishing to your app: upload once, set a caption per network, get a person's approval, and publish through every network's official API. One base URL, bearer auth, JSON both ways.

Base URLhttps://publish.debuginit.com
AuthAuthorization: Bearer <DebugInit ID access token>
FormatJSON; uploads are the raw file
Machine-readable/openapi.json (OpenAPI 3.1): import it into Postman, Insomnia or a code generator
AI agentsMCP server at https://publish.debuginit.com/mcp, see MCP

Built in-house by DebugInit. Pulse Publish is not a wrapper around another posting service. The DebugInit team wrote every network connector, the approval engine and this API in Rust, and it calls each network's official API directly.

Before you start

Get access.

  1. Create a DebugInit ID

    Sign up at id.mypulse.buzz and verify your email. It's the identity your integration calls with.

  2. Request API access

    Tell us what you're building through debuginit.com/contact, with your verified email, the networks you need, and your web origin if you'll call from a browser.

  3. Get your access level

    We enable your ID with read or write access and connect the network accounts you'll publish to. GET /v1/me shows what you have.

Quick start · 5 minutes

From a token to a live post.

The whole journey once, end to end. Pick your language in any code box; the rest of the page follows.

  1. Set up

    Get an access token for your DebugInit ID (see Authentication) and keep it in an environment variable.

    export PP=https://publish.debuginit.com
    export TOKEN=…   # your DebugInit ID access token
  2. Check your access

    Expect your account, and "write": true if you can create and approve.

    curl "$PP/v1/me" \
      -H "Authorization: Bearer $TOKEN"
  3. See the connected accounts

    Only networks with "connected": true can be posted to. See Connected accounts.

    curl "$PP/v1/networks" \
      -H "Authorization: Bearer $TOKEN"
  4. Upload the video and cover

    curl -X POST "$PP/v1/media?name=edge-part2.mp4" \
      -H "Authorization: Bearer $TOKEN" \
      --data-binary @edge-part2.mp4
    
    curl -X POST "$PP/v1/media?name=edge-part2-cover.png" \
      -H "Authorization: Bearer $TOKEN" \
      --data-binary @edge-part2-cover.png
  5. Create the post

    One default caption, overrides where a network needs its own, and a time.

    curl -X POST "$PP/v1/posts" \
      -H "Authorization: Bearer $TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
      "id": "edge-part2",
      "at": "2026-11-20T09:30:00+05:30",
      "title": "Why edge systems",
      "video": "edge-part2.mp4",
      "cover": "edge-part2-cover.png",
      "targets": ["linkedin", "x", "pinterest"],
      "caption": "Part 2 · Why edge systems keep working when the cloud doesn't.",
      "captions": { "x": "Part 2 · Why edge systems. Part 1: https://lnkd.in/… #EdgeComputing" },
      "settings": { "pinterest": { "title": "Why Edge Systems Matter (Part 2 of 3)", "link": "https://wayworks.space" } }
    }'
  6. Preview it for a network

    The exact text that will go, and its length as that network counts it.

    curl "$PP/v1/posts/edge-part2/x" \
      -H "Authorization: Bearer $TOKEN"
  7. Approve it

    One call per network. This is the human step: put it behind an Approve button in your app, pressed by the person accountable for the post.

    curl -X POST "$PP/v1/posts/edge-part2/x/approve" \
      -H "Authorization: Bearer $TOKEN"
  8. Publish now, or let the time come

    Approved posts go out at at. To go now:

    curl -X POST "$PP/v1/posts/edge-part2/x/publish" \
      -H "Authorization: Bearer $TOKEN"
  9. Read the result

    status becomes posted with the live url, or failed with the network's own error.

    curl "$PP/v1/posts/edge-part2" \
      -H "Authorization: Bearer $TOKEN"

That's the whole API. Everything else is detail: per-network settings, connected accounts and the reference below.

Authentication

Every /v1 call except /v1/health needs Authorization: Bearer <token>, where the token is a DebugInit ID access token for an account that has API access.

Issuerhttps://id.mypulse.buzz (OpenID configuration)
FlowOAuth 2.1 authorization code with PKCE (S256)
Scopesopenid email profile
CheckedOn each call (cached for a minute): the token must be valid and its verified email must have access

Apps acting for a person sign them in with DebugInit ID and send their token. Server-to-server integrations use a token for a dedicated DebugInit ID created for that service.

Keep tokens out of URLs, logs and front-end bundles. In CI, store them as secrets. A 401 means the token expired or was revoked: get a fresh one.

Access levels.

Read lets you list networks, posts, previews, media and activity. Write also lets you upload, create, delete, approve and publish. Without write access every POST and DELETE answers 403 and nothing changes. GET /v1/me returns "write". In the reference, calls that need it are marked write access.

Concepts

Posts and targets.

A post is one video with a title, a time and captions. Each network it goes to is a target: post × network. Targets are approved, published and reported one by one, so LinkedIn can be live while X still waits for approval.

Posts you create have "source": "api" and can be deleted until something is posted. Posts the workspace manages itself show "source": "config"; you can read and approve them, not delete them.

Approval#

Approving stores a fingerprint of the title, caption, settings, video and cover. Before anything is sent, the fingerprint is checked again. If it differs, approval reads changed and the target is refused until a person approves it again.

no→ approve →ok→ edit anything →changed→ approve again →ok

Lifecycle#

waiting→ time comes →due→posting→postedorfailed

A failed target keeps its error. Fix the cause (a caption that's too long, an expired sign-in) and publish it again.

Scheduling#

Approved targets go out at their at time; the scheduler checks every minute. GET /v1/me shows "schedule": "on" while scheduled publishing runs. publish sends immediately whatever the time.

Guides

Recipes for real posts.

Post to a LinkedIn Page, for an audience#

Target linkedin_page and say which Page in settings: an id, a link, a vanity name or the Page's name. A Showcase Page works the same way. An audience narrows who sees it in their feed; LinkedIn needs more than 300 matching members.

curl -X POST "$PP/v1/posts" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "id": "edge-page", "at": "2026-11-21T09:30:00+05:30", "title": "Why edge systems",
  "video": "edge-part2.mp4", "targets": ["linkedin_page"],
  "caption": "Why edge systems keep working when the cloud doesn't.",
  "settings": { "linkedin_page": { "page": "143896897",
    "audience": { "locations": ["India"], "seniorities": ["director", "vp", "cxo"] } } }
}'

Audience keys: locations, seniorities (unpaid, training, entry, senior, manager, director, vp, cxo, partner, owner), industries, job_functions.

One caption per network#

caption is the default; captions overrides it for any network. Check with the preview call: on X a link always counts as 23 characters and an emoji as 2, so the raw length isn't what X sees.

curl "$PP/v1/posts/edge-part2/x" \
  -H "Authorization: Bearer $TOKEN"

Pinterest Pins#

Pins have their own title (up to 100), a destination link and alt text (up to 500). The board is the one chosen when Pinterest was connected.

Response
"targets": ["pinterest"],
"captions": { "pinterest": "Six reasons edge systems matter, with real examples. Part 2 of 3." },
"settings": { "pinterest": {
  "title": "Why Edge Systems Matter: 6 Real Examples (Part 2 of 3)",
  "link": "https://wayworks.space",
  "alt_text": "Vertical explainer video on six reasons edge systems matter"
} }

Connected accounts#

Pulse Publish holds the sign-ins to each network, so your app never stores a LinkedIn or X token. GET /v1/networks lists them with connected, the account name and expires_in_days.

  1. To connect an account, or reconnect one before it expires, call signin.
  2. Send the account's owner to the returned open URL. They approve on the network's own screen; the network returns to Pulse Publish, which shows "Connected".
  3. Check GET /v1/networks again. A network not yet enabled for your access answers 409: ask us to enable it.
curl -X POST "$PP/v1/networks/linkedin/signin" \
  -H "Authorization: Bearer $TOKEN"

Call from a browser#

Browsers may call the API from the web origins registered for your integration (CORS); tell us yours when you request access. Use the signed-in person's own DebugInit ID token.

JavaScript
// inside the Pulse app: token = the signed-in person's DebugInit ID access token
const res = await fetch("https://publish.debuginit.com/v1/posts", {
  headers: { Authorization: "Bearer " + token },
});
if (res.status === 401) return signInAgain();
const { posts } = await res.json();

Automate from CI#

Render the video in CI, upload it, queue it, and leave approval to a person. Keep the token in the CI's secrets.

GitHub Actions
# .github/workflows/queue-video.yml
on: { workflow_dispatch: {} }
jobs:
  queue:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: |
          curl -fsS -X POST "$PP/v1/media?name=weekly.mp4" -H "Authorization: Bearer $TOKEN" --data-binary @out/weekly.mp4
          curl -fsS -X POST "$PP/v1/posts" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
            -d @post.json   # approval stays with a person
        env:
          PP: https://publish.debuginit.com
          TOKEN: ${{ secrets.PULSE_PUBLISH_TOKEN }}
MCP · Model Context Protocol

Let an AI agent do the busywork.

Pulse Publish is also an MCP server. Claude, Cursor, VS Code or any MCP client can check accounts, draft captions within each network's limits, upload and queue posts, and read results, then stop and ask a person before anything is approved or published.

Your AI clientClaude · Cursor · VS Code
Pulse Publishsame rules as the REST API
11 networksLinkedIn, X, YouTube…
Endpointhttps://publish.debuginit.com/mcp
TransportStreamable HTTP, JSON responses, stateless
Protocol2026-07-28 and 2025-11-25 · 2025-06-18 · 2025-03-26
AuthAuthorization: Bearer <DebugInit ID access token>
Offers14 tools · 4 resources and 2 templates · 3 prompts

See an agent at work#

A real sequence of tool calls. The agent does the legwork and hands the decision back to you.

AI client · connected to pulse-publish

YouQueue the edge-systems video for LinkedIn and X, tomorrow 9:30, captions in our voice.

  1. whoamiwrite access · schedule on
  2. list_networkslinkedin ✓ · x ✓ · pinterest ✓
  3. list_mediaedge-part2.mp4 · edge-part2-cover.png
  4. create_postedge-part2 queued for Tue 09:30 · approval: no
  5. preview_post · x214 of 280 characters (links count as 23) · fits
  6. preview_post · linkedin1,180 of 3,000 characters · fits

Approval needed. approve_post publishes in your name, so the agent stops and shows you both captions.

  1. approve_post · xapproval: ok
  2. approve_post · linkedinapproval: ok

AgentApproved. Both go out Tue 09:30; I'll check get_post for the live links after.

AgentNothing was approved. The post waits in your queue until you say so.

The human stays in charge. approve_post and publish_now are marked as write tools, and the server's instructions tell agents never to call them without your go-ahead. Without write access they refuse anyway.

Connect your client#

Pick your client, paste, and set your token. Then ask it something like "what's waiting for approval in Pulse Publish?"

# Claude Code
claude mcp add --transport http pulse-publish https://publish.debuginit.com/mcp \
  --header "Authorization: Bearer $TOKEN"

Tool explorer#

Every tool runs the same code as its REST call. Pick one to see its schema, hints and an example call.

Who am I whoami

Who the token belongs to, whether writing is on (approve/publish/create need it), and whether the schedule runs.

read-only
{
  "id": 1,
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "arguments": {},
    "name": "whoami"
  }
}

Resources and prompts#

ResourceWhat
pulse-publish://guideThe workflow and the approval rule, written for an AI client
pulse-publish://posts · …/posts/{id} · …/posts/{id}/{network}The queue, one post, one network's preview
pulse-publish://networks · pulse-publish://mediaConnected accounts and limits · uploaded files
PromptWhat
draft_captionsOne caption per network for a topic, inside each network's limits
review_queueWhat's waiting for approval, what failed and why, what goes out next
queue_videoUpload, create and preview a post, then stop for your approval

Protocol details#

  • 2026-07-28 clients send _meta["io.modelcontextprotocol/protocolVersion"] and clientCapabilities on every request, plus the MCP-Protocol-Version, Mcp-Method and (for tools/call, resources/read, prompts/get) Mcp-Name headers. A mismatch answers 400 with -32020. server/discover describes the server.
  • Older clients call initialize; the server answers with the version they asked for when it supports it, otherwise 2025-11-25. Notifications get 202. No session id is issued.
  • Unsupported versions: 400 with -32022 and the supported list. Unknown methods: -32601. One JSON-RPC message per POST; GET and DELETE answer 405.
  • 401s carry WWW-Authenticate pointing to the protected-resource metadata (RFC 9728). Browser origins must be registered; others get 403.
curl
# a raw 2026-07-28 call, for testing
curl "$PP/mcp" -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
  -H "MCP-Protocol-Version: 2026-07-28" -H "Mcp-Method: tools/call" -H "Mcp-Name: list_posts" \
  -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "list_posts", "arguments": {},
        "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {} } } }'
API reference

Every call.

17 calls. Examples assume PP, TOKEN and H from the set-up step. Network ids: linkedin, linkedin_page, facebook, instagram, threads, bluesky, mastodon, pinterest, x, youtube, tiktok.

GET/v1/healthpublic

Health#

Public, no token. Use it for uptime checks.

curl "$PP/v1/health" \
  -H "Authorization: Bearer $TOKEN"
Response
{ "ok": true, "service": "pulse-publish", "version": "0.1.0", "config_error": false }
GET/v1/meread

Who am I#

Who the token belongs to, whether you have write access, and whether scheduled publishing runs.

curl "$PP/v1/me" \
  -H "Authorization: Bearer $TOKEN"
Response
{ "caller": "you@yourcompany.com", "root": false, "write": true, "owner": "Pulse", "schedule": "on" }
GET/v1/networksread

List networks#

Every network: connected or not, the account, whether its keys are set, the sign-in type and redirect URL, days until the sign-in ends, and its limits.

curl "$PP/v1/networks" \
  -H "Authorization: Bearer $TOKEN"
Response
{ "networks": [ { "id": "linkedin", "name": "LinkedIn", "connected": true, "account": "Sanjay shah",
    "keys_set": true, "sign_in": "oauth", "redirect_url": "https://publish.debuginit.com/callback/linkedin",
    "expires_in_days": 57, "expired": false, "limits": { "caption_max_chars": 3000, … } }, … ] }
POST/v1/networks/{network}/signinwrite access

Start a sign-in#

Returns the network's consent URL. Open it in a browser; the network sends the browser back to /callback/{network}, which shows "Connected".

curl -X POST "$PP/v1/networks/linkedin/signin" \
  -H "Authorization: Bearer $TOKEN"
Response
{ "open": "https://www.linkedin.com/oauth/v2/authorization?…", "redirect_url": "https://publish.debuginit.com/callback/linkedin" }
POST/v1/networks/{network}/connectwrite access

Connect without a browser#

For Bluesky and Mastodon accounts set up for your access: they sign in with a handle and app password, no browser step.

curl -X POST "$PP/v1/networks/bluesky/connect" \
  -H "Authorization: Bearer $TOKEN"
Response
{ "connected": "bluesky", "account": "debuginit.bsky.social" }
DELETE/v1/networks/{network}write access

Forget a sign-in#

Removes the stored sign-in for one network; posting to it stops until it is connected again.

curl -X DELETE "$PP/v1/networks/x" \
  -H "Authorization: Bearer $TOKEN"
Response
{ "disconnected": "x" }
GET/v1/mediaread

List media#

Videos and covers uploaded to this server.

curl "$PP/v1/media" \
  -H "Authorization: Bearer $TOKEN"
Response
{ "media": [ { "name": "edge-part2.mp4", "bytes": 22295769 } ] }
POST/v1/media?name={file}write access

Upload a video or cover#

The raw file is the body. Names use letters, digits and . - _ and end in .mp4, .mov, .png, .jpg or .jpeg. A name can't be reused (409); files can be up to 500 MB (413).

curl -X POST "$PP/v1/media?name=edge-part2.mp4" \
  -H "Authorization: Bearer $TOKEN" \
  --data-binary @edge-part2.mp4

curl -X POST "$PP/v1/media?name=edge-part2-cover.png" \
  -H "Authorization: Bearer $TOKEN" \
  --data-binary @edge-part2-cover.png
Response
{ "name": "edge-part2.mp4", "bytes": 22295769 }
GET/v1/postsread

List the queue#

Every post with, per network, its approval (no · ok · changed), status (waiting · due · posting · posted · failed), and the live url or the error.

curl "$PP/v1/posts" \
  -H "Authorization: Bearer $TOKEN"
Response
{ "posts": [ { "id": "edge-part2", "at": "2026-11-20T04:00:00Z", "title": "Why edge systems", "source": "api",
    "video": "edge-part2.mp4", "cover": "edge-part2-cover.png",
    "targets": [ { "network": "x", "approval": "ok", "status": "posted", "url": "https://x.com/…/status/…",
                   "approved_at": "…", "posted_at": "…", "error": null } ] } ] }
POST/v1/postswrite access

Create a post#

Fields: id (a–z, 0–9 and -, unique) · at (RFC 3339 time with its offset) · title · video and cover (names from /v1/media) · targets (network ids) · caption (the default text) · captions (per-network text) · settings (per-network options, see the guides).

curl -X POST "$PP/v1/posts" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "id": "edge-part2",
  "at": "2026-11-20T09:30:00+05:30",
  "title": "Why edge systems",
  "video": "edge-part2.mp4",
  "cover": "edge-part2-cover.png",
  "targets": ["linkedin", "x", "pinterest"],
  "caption": "Part 2 · Why edge systems keep working when the cloud doesn't.",
  "captions": { "x": "Part 2 · Why edge systems. Part 1: https://lnkd.in/… #EdgeComputing" },
  "settings": { "pinterest": { "title": "Why Edge Systems Matter (Part 2 of 3)", "link": "https://wayworks.space" } }
}'
Response
201 — the new post, in the same shape as GET /v1/posts/{id}
GET/v1/posts/{id}read

Get one post#

One post, same shape as in the list. Poll this after publishing.

curl "$PP/v1/posts/edge-part2" \
  -H "Authorization: Bearer $TOKEN"
Response
{ "id": "edge-part2", "targets": [ … ] }
DELETE/v1/posts/{id}write access

Delete a post#

Only posts created through the API, and only until something has been posted.

curl -X DELETE "$PP/v1/posts/edge-part2" \
  -H "Authorization: Bearer $TOKEN"
Response
{ "deleted": "edge-part2" }
GET/v1/posts/{id}/{network}read

Preview for one network#

Exactly what would be sent: title, caption, its length as that network counts it, settings, the limits check, and the fingerprint an approval would cover.

curl "$PP/v1/posts/edge-part2/x" \
  -H "Authorization: Bearer $TOKEN"
Response
{ "network": "x", "title": "Why edge systems", "caption": "Part 2 · Why edge systems. Part 1: https://lnkd.in/… #EdgeComputing",
  "caption_length": 74, "caption_max": 280, "fits": true, "settings": {}, "fingerprint": "9f2c…" }
POST/v1/posts/{id}/{network}/approvewrite access

Approve#

Approves exactly the current content for one network. If anything changes later, approval becomes "changed" and nothing goes out.

curl -X POST "$PP/v1/posts/edge-part2/x/approve" \
  -H "Authorization: Bearer $TOKEN"
Response
{ "id": "edge-part2", "targets": [ { "network": "x", "approval": "ok", "status": "waiting" } ] }
POST/v1/posts/{id}/{network}/unapprovewrite access

Withdraw approval#

Stops a post that hasn't gone out yet.

curl -X POST "$PP/v1/posts/edge-part2/x/unapprove" \
  -H "Authorization: Bearer $TOKEN"
Response
{ "id": "edge-part2", "targets": [ { "network": "x", "approval": "no" } ] }
POST/v1/posts/{id}/{network}/publishwrite access

Publish now#

Publishes now instead of at its time. It must be approved. Answers 202 at once; follow the result with GET /v1/posts/{id}.

curl -X POST "$PP/v1/posts/edge-part2/x/publish" \
  -H "Authorization: Bearer $TOKEN"
Response
202 { "publishing": "edge-part2/x", "check": "/v1/posts/edge-part2" }
GET/v1/activity?lines=50read

Activity log#

The latest log lines: who approved what, what was posted, errors. Up to 500 lines.

curl "$PP/v1/activity?lines=50" \
  -H "Authorization: Bearer $TOKEN"
Response
{ "lines": [ "2026-11-20 09:30:02 x: posted edge-part2 → https://x.com/…", … ] }

Errors and status codes.

Errors are JSON with one plain sentence; when a network refused something, its own message is included.

Response
409 { "error": "edge-part2/x changed since it was approved — approve again" }
502 { "error": "x: HTTP 403 Forbidden: {\"detail\":\"You are not permitted to perform this action.\"}" }
CodeMeaningWhat to do
200 · 201 · 202Done · created · startedFor 202, poll GET /v1/posts/{id}
400Something in the request is wrongThe message names the field
401No token, or it expired or was revokedGet a fresh DebugInit ID token
403Your access is read-only, or your DebugInit ID isn't enabledRequest access
404No such post, network or fileCheck the id
409Not possible now: not approved, changed since approval, already posted, name taken, network not enabledApprove again, rename, or ask us
413Upload over 500 MBCompress the file
502DebugInit ID couldn't be reachedRetry with backoff

Network limits.

The limits the API checks before anything is sent. The preview call checks them for you.

IdNetworkCaptionTitleVideoNotes
linkedinLinkedIn300020030 mincustom cover
linkedin_pageLinkedIn Page300020030 mincustom cover
facebookFacebook Page6320625590 scustom cover
instagramInstagram2200—15 mincustom cover
threadsThreads500—5 minpublic video URL
blueskyBluesky300—3 min—
mastodonMastodon500——custom cover, links count as 23
pinterestPinterest80010015 mincustom cover
xX280—20 minlinks count as 23, emoji count as 2
youtubeYouTube Shorts50001003 mincustom cover
tiktokTikTok2200—10 min—
Before you ship

Integration checklist.

What a solid integration handles. Ticks are remembered in this browser.