Pulse API
Pulse lets the members of your product connect their own LinkedIn, Instagram, Facebook and YouTube accounts and post to them. You call the API from your server. Your member never needs a Pulse account.
Words we use. A partner is you. A member is one of your users. A connection is one social account a member linked.
All examples use https://YOUR-PULSE-URL as the base address and $PULSE_KEY as your API key. Replace both. Every API path below starts with /v1/partner. Bodies are JSON, and every time is an ISO 8601 time in UTC.
Quickstart
Five calls, from nothing to a scheduled post.
export PULSE_KEY="fnk_your_key_here"
export PULSE_URL="https://YOUR-PULSE-URL"
1. Create a member
Requestcurl -X POST "$PULSE_URL/v1/partner/members" \
-H "Authorization: Bearer $PULSE_KEY" \
-H "Content-Type: application/json" \
-d '{"external_id": "user_8412", "name": "Amara Okafor"}'
Response 201
{
"id": "m_4f9c2a71e0b35d8c6a1f9e24",
"external_id": "user_8412",
"name": "Amara Okafor",
"created_at": "2026-10-07T09:30:00.000Z",
"limits": { "max_accounts": 10, "max_per_network": 3, "networks": ["linkedin", "instagram", "facebook", "youtube"] }
}
2. Create a connect session
Requestcurl -X POST "$PULSE_URL/v1/partner/members/m_4f9c2a71e0b35d8c6a1f9e24/connect-session" \
-H "Authorization: Bearer $PULSE_KEY" \
-H "Content-Type: application/json" \
-d '{"return_url": "https://app.example.com/settings/social"}'
Response 201
{
"url": "https://YOUR-PULSE-URL/connect/eyJwYXJ0bmVyIjoiYWNtZSJ9.q3TzV8",
"expires_at": "2026-10-07T10:00:00.000Z"
}
3. Open the url
Send your member to the url: a link, a redirect, a popup or an iframe (see The connect window). They sign in to each network on its own page. There is nothing to call here. The link works for 30 minutes by default.
4. List connections
Requestcurl "$PULSE_URL/v1/partner/members/m_4f9c2a71e0b35d8c6a1f9e24/connections" \
-H "Authorization: Bearer $PULSE_KEY"
Response 200
{
"connections": [
{ "id": "c_66f1a2b3c4d5e6f708192a3b", "network": "linkedin", "label": "Amara Okafor", "status": "active", "connected_at": "2026-10-07T09:41:12.000Z" }
]
}
5. Create a post
Requestcurl -X POST "$PULSE_URL/v1/partner/members/m_4f9c2a71e0b35d8c6a1f9e24/posts" \
-H "Authorization: Bearer $PULSE_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "We just opened our new studio. Come and say hello.", "networks": ["linkedin"]}'
Response 201
{
"id": "5b0e8f1c-3a7d-4c52-9e64-0d2f7a91b8c3",
"status": "scheduled",
"scheduled_at": "2026-10-07T09:45:30.000Z",
"text": "We just opened our new studio. Come and say hello.",
"networks": [
{ "network": "linkedin", "connection_id": "c_66f1a2b3c4d5e6f708192a3b", "status": "scheduled", "external_url": null, "error": null }
]
}
With no scheduled_at, the post goes out about two minutes from now. The member had one LinkedIn account, so it was used without being named (see Posts).
Authentication
Send your key on every request in the Authorization header:
Authorization: Bearer fnk_your_key_here
Keep the key on your server. Do not put it in a browser, a mobile app or a public repository. If it leaks, email us and we will replace it.
Your key only works on paths that start with /v1/partner (and with the connect window links you create). Anywhere else it returns 403 forbidden_scope. It only ever sees your own members.
A missing, wrong or switched-off key returns 401 unauthorized. An id that belongs to someone else returns 404 not_found, the same as an id that does not exist. A path that does not exist under /v1/partner returns 404 not_found with the message "No such endpoint."
Ids you get back: a member id looks like m_ followed by 24 characters, a connection id starts with c_, and a post id is a UUID. Treat all of them as opaque text.
Members
A member is one of your users. You choose the external_id: use your own user id, 1 to 128 characters. Pulse gives back its own id for all later calls.
Create a member
POST /v1/partner/members
| Field | Type | Notes |
|---|---|---|
external_id | string | Required. 1 to 128 characters, no control characters. Your own id for this member. |
name | string | Optional. Up to 120 characters. |
email | string | Optional. Must look like an email address. It is stored and returned, never sent to the networks. |
This call is safe to repeat. If a member with the same external_id already exists, you get 200 and the existing member, and name and email in the repeat call are ignored (use update to change them). A new member returns 201. A member always has limits: the numbers that apply to them right now (see Limits). email is only present if you set one. name is null if you did not.
curl -X POST "https://YOUR-PULSE-URL/v1/partner/members" \
-H "Authorization: Bearer $PULSE_KEY" \
-H "Content-Type: application/json" \
-d '{"external_id": "user_8412", "name": "Amara Okafor", "email": "amara@example.com"}'
Response 201 (200 if it already existed)
{
"id": "m_4f9c2a71e0b35d8c6a1f9e24",
"external_id": "user_8412",
"name": "Amara Okafor",
"email": "amara@example.com",
"created_at": "2026-10-07T09:30:00.000Z",
"limits": { "max_accounts": 10, "max_per_network": 3, "networks": ["linkedin", "instagram", "facebook", "youtube"] }
}
List members
GET /v1/partner/members?limit=50&cursor=. limit is a whole number from 1 to 100 (default 50). Members come oldest first. Send the next_cursor from one answer as cursor in the next call. When next_cursor is null, you have everything. A cursor that is not one of your member ids returns 422 validation.
curl "https://YOUR-PULSE-URL/v1/partner/members?limit=2" \
-H "Authorization: Bearer $PULSE_KEY"
Response 200
{
"members": [
{ "id": "m_4f9c2a71e0b35d8c6a1f9e24", "external_id": "user_8412", "name": "Amara Okafor", "created_at": "2026-10-07T09:30:00.000Z", "limits": { "max_accounts": 10, "max_per_network": 3, "networks": ["linkedin", "instagram", "facebook", "youtube"] } },
{ "id": "m_77b0e5c4a91d2f3068b5c7de", "external_id": "user_9100", "name": "Tomas Reyes", "created_at": "2026-10-07T09:52:40.000Z", "limits": { "max_accounts": 10, "max_per_network": 3, "networks": ["linkedin", "instagram", "facebook", "youtube"] } }
],
"next_cursor": "m_77b0e5c4a91d2f3068b5c7de"
}
Get a member
GET /v1/partner/members/:id. Returns the member and how many accounts they have connected.
curl "https://YOUR-PULSE-URL/v1/partner/members/m_4f9c2a71e0b35d8c6a1f9e24" \
-H "Authorization: Bearer $PULSE_KEY"
Response 200
{
"id": "m_4f9c2a71e0b35d8c6a1f9e24",
"external_id": "user_8412",
"name": "Amara Okafor",
"created_at": "2026-10-07T09:30:00.000Z",
"limits": { "max_accounts": 10, "max_per_network": 3, "networks": ["linkedin", "instagram", "facebook", "youtube"] },
"connections": 1
}
Update a member
PATCH /v1/partner/members/:id. Send only the fields you want to change: name, email. Send null to clear one. external_id cannot be changed.
curl -X PATCH "https://YOUR-PULSE-URL/v1/partner/members/m_4f9c2a71e0b35d8c6a1f9e24" \
-H "Authorization: Bearer $PULSE_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Amara Okafor-Smith"}'
Response 200
{
"id": "m_4f9c2a71e0b35d8c6a1f9e24",
"external_id": "user_8412",
"name": "Amara Okafor-Smith",
"created_at": "2026-10-07T09:30:00.000Z",
"limits": { "max_accounts": 10, "max_per_network": 3, "networks": ["linkedin", "instagram", "facebook", "youtube"] }
}
Delete a member
DELETE /v1/partner/members/:id?confirm=true. You cannot undo this. Without exactly confirm=true you get 400 confirm_required and nothing happens. With it, Pulse does three things in this order:
- Cancels every post of the member that is still scheduled.
- Disconnects every account the member linked.
- Removes all of the member's posts and the member.
If a scheduled post cannot be cancelled, or an account cannot be disconnected, the call stops with 502 provider_unavailable and nothing is removed from Pulse: the member and their posts are still there. Some earlier steps may already have happened, so just call it again. If a post is being prepared at that moment you get 409 conflict: try again in a few seconds.
curl -X DELETE "https://YOUR-PULSE-URL/v1/partner/members/m_4f9c2a71e0b35d8c6a1f9e24?confirm=true" \
-H "Authorization: Bearer $PULSE_KEY"
Response 204 (no body)
(empty)
Response 400 without confirm=true
{ "error": { "code": "confirm_required", "message": "Deleting a member can't be undone. Add ?confirm=true to confirm.", "field": "confirm" } }
Response 502 (an account could not be disconnected)
{ "error": { "code": "provider_unavailable", "message": "The posting service isn't answering right now. Please try again in a minute." } }
The connect window
The connect window is a page we host. Your member sees their connected accounts at the top and one button per network below. They click a network, sign in on that network's own page, and come back. All buttons stay active after a connection, so a member can add more accounts until a limit is reached. A network at its limit has its button switched off with the reason next to it. A member can also disconnect an account in the window: it asks "Yes, disconnect" or "Keep it" first. "Powered by futurizenow.co" appears in small type under the window. LinkedIn members choose between their personal profile and the company pages they manage inside the connect window; each page counts as a connected account toward the limits.
Create a connect session
POST /v1/partner/members/:id/connect-session
| Field | Type | Notes |
|---|---|---|
networks | array | Optional. A non-empty list of linkedin, instagram, facebook, youtube. Leave out for every network the member is allowed. A network that is not switched on for this member returns 422 network_not_enabled. An unknown name returns 422 validation. |
return_url | https url | Optional. Adds a "Done" button that sends the member back to you. It must be a public https link (see the link rules under Posts), otherwise 422 validation. |
expires_in | seconds | Optional. A whole number from 60 to 3600. Default 1800 (30 minutes). |
If every network in the session is already at its limit, nothing could be connected, so you get 409 limit_reached and no link. If at least one network can still take an account, you get the link.
curl -X POST "https://YOUR-PULSE-URL/v1/partner/members/m_4f9c2a71e0b35d8c6a1f9e24/connect-session" \
-H "Authorization: Bearer $PULSE_KEY" \
-H "Content-Type: application/json" \
-d '{"networks": ["linkedin", "instagram"], "return_url": "https://app.example.com/settings/social", "expires_in": 900}'
Response 201
{
"url": "https://YOUR-PULSE-URL/connect/eyJwYXJ0bmVyIjoiYWNtZSJ9.q3TzV8",
"expires_at": "2026-10-07T09:45:00.000Z"
}
Response 409 (every requested network is full)
{ "error": { "code": "limit_reached", "message": "You've reached the limit of 3 accounts on LinkedIn." } }
The link works for one member only, and only for the networks you named. Make a new session each time you want to show the window.
What the member goes through
You never call these addresses yourself. They are here so you know what happens.
- The member opens
/connect/:token. This is the window. - They press a network button. The window sends
POST /connect/:token/start, and the answer is a303redirect to that network's sign-in page. - After signing in, the network sends them to
/connect/:token/done. Pulse then checks the member's limits once more and keeps the new account only if there is room. If there is not (for example two windows were open at once), the extra account is disconnected again and the window says "That account was not kept because the limit is reached." - Pulse sends the member back to the window, which shows the result.
The same token covers every step, so the link must still be valid when the member comes back from the network.
Open it as a full page
window.location.href = url;
Open it as a popup
window.open(url, "pulse-connect", "width=480,height=720");
Open it in an iframe
<iframe src="https://YOUR-PULSE-URL/connect/TOKEN"
title="Connect your accounts"
style="width:100%;max-width:480px;height:720px;border:0"></iframe>
The window works as a page, a popup or an iframe. The network sign-in happens inside the same frame. The connect window can be framed by any site. Every other page and every API response cannot be framed.
Know when something changed: postMessage
After a member connects or disconnects an account, the window sends a message to your page (window.parent for an iframe, window.opener for a popup). It carries no secrets. It is sent only after a change, never when the window is first opened and never after a cancelled attempt. status is connected or disconnected.
{
"type": "pulse.connection",
"member_id": "m_4f9c2a71e0b35d8c6a1f9e24",
"network": "linkedin",
"status": "connected"
}
Listen for it like this, then confirm with GET /connections before you trust it:
window.addEventListener("message", (event) => {
if (event.origin !== "https://YOUR-PULSE-URL") return;
if (event.data && event.data.type === "pulse.connection") {
// event.data.member_id, event.data.network, event.data.status
refreshConnections(event.data.member_id);
}
});
event.origin. The message is sent to any origin, so do not act on a message from a page you do not know.return_url
If you passed return_url, the window shows a "Done" button that sends the member there with a status added to the address (any status already in your address is replaced):
| Status | Meaning |
|---|---|
connected | The member connected an account while this window was open. |
cancelled | Anything else: the member left without connecting an account, or the new account was not kept because of a limit. |
https://app.example.com/settings/social?status=connected
There is no other status. The status is a hint for your screen. The truth is in the list of connections.
Links that do not work
| HTTP | What the member sees | When |
|---|---|---|
| 410 | "This link has expired" | The link is past its expires_at. This also applies to the sign-in steps. |
| 400 | "This link doesn't work" | The link was changed or is not a link we made. The page does not say which. |
| 410 | "This link doesn't work" | The member was deleted, or all of your keys were switched off. |
| 429 | "Too many tries" | More than 120 requests an hour for one link. A Retry-After header says how long to wait. |
| 502 | "We couldn't check your accounts just now." | The posting service did not answer. The buttons are hidden until it does. |
These are plain pages, not JSON. The window does not count against your 60 requests a minute (see Rate limits).
Connections
List a member's connections
GET /v1/partner/members/:id/connections. status is active, or needs_reconnect when the network has asked the member to sign in again. Open a new connect session for them. label is the account name when the network gives one, otherwise the network's name.
curl "https://YOUR-PULSE-URL/v1/partner/members/m_4f9c2a71e0b35d8c6a1f9e24/connections" \
-H "Authorization: Bearer $PULSE_KEY"
Response 200
{
"connections": [
{ "id": "c_66f1a2b3c4d5e6f708192a3b", "network": "linkedin", "label": "Amara Okafor", "status": "active", "connected_at": "2026-10-07T09:41:12.000Z" },
{ "id": "c_9d3e7a50b1c2486f0a5e1d37", "network": "youtube", "label": "Okafor Studio", "status": "needs_reconnect", "connected_at": "2026-09-28T14:03:55.000Z" }
]
}
This call also checks the member's accounts against the limits. An account that appeared while no room was left is disconnected and does not show up. If the posting service does not answer, you get 502 provider_unavailable.
Disconnect one
DELETE /v1/partner/members/:id/connections/:cid. This disconnects the account at the posting service. It does not need confirm=true. A post that was scheduled for that account will not go out through another one. If the account cannot be disconnected, you get 502 provider_unavailable and the connection stays. An unknown connection id, or one that belongs to another member, returns 404 not_found.
curl -X DELETE "https://YOUR-PULSE-URL/v1/partner/members/m_4f9c2a71e0b35d8c6a1f9e24/connections/c_9d3e7a50b1c2486f0a5e1d37" \
-H "Authorization: Bearer $PULSE_KEY"
Response 204 (no body)
(empty)
Limits
Limits decide how many accounts a member can connect and to which networks. There are three levels. The first one that is set wins:
- Member. Set by you with the call below.
- Partner default. Set by us on your key. Ask us if you want different defaults for all your members.
- Global default.
max_accounts10 in total,max_per_network3, and every network except X (linkedin,instagram,facebook,youtube). X is only available if we switch it on for you.
The numbers in force for a member are always in the limits field of the member, with all three fields filled in (the "effective" values).
422 validation and the message "max_accounts can be at most 10 for your account. Ask us if you need more." (the number is your own default). It is not quietly reduced. The same goes for networks: a network outside your partner default is refused with 422 network_not_enabled. If you need more than your default, ask us to change the default on your key.When a limit is reached, the button for that network in the connect window is switched off with the reason next to it, and a connect session for networks that are all full answers 409 limit_reached. Lowering a limit does not disconnect accounts the member already has. It only stops new ones from being kept. A member's own numbers can only lower your account's default, never raise it: if your default is lowered later, a member who was set higher is held to the new number. networks in a limits call must name at least one network (send null to go back to your default); an empty list returns 422 validation.
Set a member's limits
PUT /v1/partner/members/:id/limits. All fields are optional. A field you leave out stays as it is. Send null to clear a field and fall back to the next level.
| Field | Type | Notes |
|---|---|---|
max_accounts | integer | Accounts across all networks. A whole number from 1 up to your partner default. |
max_per_network | integer | Accounts on any one network. A whole number from 1 up to your partner default. |
networks | array | Networks this member may connect and post to. Each must be switched on for your account. |
curl -X PUT "https://YOUR-PULSE-URL/v1/partner/members/m_4f9c2a71e0b35d8c6a1f9e24/limits" \
-H "Authorization: Bearer $PULSE_KEY" \
-H "Content-Type: application/json" \
-d '{"max_accounts": 4, "max_per_network": 1, "networks": ["linkedin", "youtube"]}'
Response 200 (the limits now in force for this member)
{
"max_accounts": 4,
"max_per_network": 1,
"networks": ["linkedin", "youtube"]
}
Response 422 (above your partner default)
{ "error": { "code": "validation", "message": "max_accounts can be at most 10 for your account. Ask us if you need more.", "field": "max_accounts" } }
Response 422 (X is not switched on)
{ "error": { "code": "network_not_enabled", "message": "X is not switched on for your account.", "field": "networks" } }
To clear one field, send null:
curl -X PUT "https://YOUR-PULSE-URL/v1/partner/members/m_4f9c2a71e0b35d8c6a1f9e24/limits" \
-H "Authorization: Bearer $PULSE_KEY" \
-H "Content-Type: application/json" \
-d '{"max_per_network": null}'
Response 200 (max_per_network is back to your partner default, here the global 3)
{
"max_accounts": 4,
"max_per_network": 3,
"networks": ["linkedin", "youtube"]
}
Posts
A post goes out through the accounts this member has connected, on each network you list. If the member has not connected an account on a network you list, the request is refused with a plain message. A network must be switched on for the member.
Create a post
POST /v1/partner/members/:id/posts
| Field | Type | Notes |
|---|---|---|
text | string | Required. The text for every network. Up to 20,000 characters, and each network has its own shorter limit (see the table below). |
networks | array | Required. Where to post, for example ["linkedin","facebook"]. Repeats are ignored. A network outside the member's effective networks returns 422 network_not_enabled. |
connection_ids | array | Optional. Which account to use, when you need to say. Ids come from the list of connections. See "Which account is used" below. |
texts | object | Optional. Different text for one network, for example {"linkedin": "..."}. Only networks that are in networks are allowed. Other networks use text. |
media_urls | array | Optional. Up to 10 links to images or video that we can fetch. See the link rules below. |
youtube_title | string | The video title. Up to 100 characters, no < or >. |
scheduled_at | ISO 8601 | Optional. When to post, like 2026-11-01T09:00:00Z. Leave out to post in about 2 minutes. It cannot be in the past (a minute of slack is allowed) and cannot be more than a year ahead (366 days). Otherwise 422 validation. |
Which account is used
- If the member has one account on a network, it is used automatically. You do not need
connection_ids. - If the member has several accounts on a network, you must name exactly one of them in
connection_ids. With none named, or more than one named for the same network, you get422 ambiguous_account. - A connection id that is not one of this member's own accounts returns
404 not_found, the same as one that never existed. - A connection on a network that is not in
networksreturns422 validation(fieldconnection_ids). - No account on a network you list, or the account needs reconnecting, returns
422 not_connected. Open a new connect session for the member. - The account you pick is the one the post goes out through. If it is disconnected before the post goes out, the post does not go out through another account.
Rules for media links
Every link in media_urls (and every return_url) must be a public https link. These are refused with 422 validation:
- Anything that is not
https. - An IP address instead of a name, such as
https://192.168.0.1/a.mp4orhttps://[::1]/a.mp4. localhost, and internal names ending in.local,.localhost,.internal,.lan,.home,.corpor.intranet, or a name with no dot.- A link with a user name or password in it.
- A port other than 443.
- A link longer than 2,048 characters.
curl -X POST "https://YOUR-PULSE-URL/v1/partner/members/m_4f9c2a71e0b35d8c6a1f9e24/posts" \
-H "Authorization: Bearer $PULSE_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "Our October workshop is open for booking.",
"texts": { "linkedin": "Our October workshop is open for booking. Details and dates in the comments." },
"media_urls": ["https://cdn.example.com/workshop.mp4"],
"networks": ["linkedin", "youtube"],
"connection_ids": ["c_66f1a2b3c4d5e6f708192a3b"],
"youtube_title": "October workshop: what to expect",
"scheduled_at": "2026-10-09T08:00:00Z"
}'
Response 201
{
"id": "5b0e8f1c-3a7d-4c52-9e64-0d2f7a91b8c3",
"status": "scheduled",
"scheduled_at": "2026-10-09T08:00:00.000Z",
"text": "Our October workshop is open for booking.",
"networks": [
{ "network": "linkedin", "connection_id": "c_66f1a2b3c4d5e6f708192a3b", "status": "scheduled", "external_url": null, "error": null },
{ "network": "youtube", "connection_id": "c_9d3e7a50b1c2486f0a5e1d37", "status": "scheduled", "external_url": null, "error": null }
]
}
In this example the member has one YouTube account, so it was used without being named.
Response 422 (a rule was broken){ "error": { "code": "validation", "message": "LinkedIn allows 3000 characters — this is 3412." } }
Response 422 (several accounts, none named)
{ "error": { "code": "ambiguous_account", "message": "This member has 2 LinkedIn accounts. Say which one with connection_ids.", "field": "networks" } }
Response 422 (nothing connected on a network)
{ "error": { "code": "not_connected", "message": "This member hasn't connected Instagram (or it needs reconnecting).", "field": "networks" } }
What a post looks like
| Field | Notes |
|---|---|
id | The post id (a UUID). |
status | One of scheduled, publishing, published, partial (some networks published, some failed) or failed. |
scheduled_at | When the post goes out. null if there is none. |
text | The text you sent. |
error | Only present when something went wrong with the post as a whole. A plain sentence. |
networks[].network | The network. |
networks[].connection_id | The account this network uses. null if none was fixed. |
networks[].status | scheduled, publishing, published or failed. Each network has its own. |
networks[].external_url | The link to the live post, once published. Otherwise null. |
networks[].error | A plain sentence when that network failed. Otherwise null. |
Which accounts a member can connect
Members connect their own accounts, but each network decides which kinds of account an app may post to. Tell your members before they start, so the sign-in does not surprise them.
| Network | What the member needs |
|---|---|
| Their personal profile works. If they also manage a company page, they can pick it during sign-in when LinkedIn offers it. | |
| A Business or Creator account. Instagram does not let any app post to a personal account. Switching is free in the Instagram app and takes a minute. | |
| A Page they manage. Facebook does not let any app post to a personal profile or timeline. A member with no Page can create one for free. | |
| YouTube | Their own channel. Videos only. |
If a member signs in with an account the network will not allow, that network's connection does not complete, or a later post to it comes back failed with a plain error.
Rules for each network
These are checked when you create the post. They count the text that network will get (texts if you gave one for it, otherwise text).
| Network | What is checked |
|---|---|
| Up to 3,000 characters. | |
Needs at least one image or video in media_urls. Up to 2,200 characters and no more than 30 hashtags. | |
| Up to 63,206 characters. | |
| YouTube | Needs at least one link in media_urls, and it must be a video. Up to 5,000 characters. youtube_title is up to 100 characters and cannot contain < or >. |
| X | Not offered unless we switch it on for you. A post that lists x otherwise returns 422 network_not_enabled. |
If a rule is broken, the 422 validation message tells you which one in plain words. A network can still refuse a post later for its own reasons, such as the type of account or the video format. When that happens, that network's status becomes failed and its error says why.
List posts
GET /v1/partner/members/:id/posts. The 100 newest posts, newest first. There is no paging.
curl "https://YOUR-PULSE-URL/v1/partner/members/m_4f9c2a71e0b35d8c6a1f9e24/posts" \
-H "Authorization: Bearer $PULSE_KEY"
Response 200
{
"posts": [
{
"id": "5b0e8f1c-3a7d-4c52-9e64-0d2f7a91b8c3",
"status": "partial",
"scheduled_at": "2026-10-09T08:00:00.000Z",
"text": "Our October workshop is open for booking.",
"networks": [
{ "network": "linkedin", "connection_id": "c_66f1a2b3c4d5e6f708192a3b", "status": "published", "external_url": "https://www.linkedin.com/feed/update/urn:li:share:7000000000000000000", "error": null },
{ "network": "youtube", "connection_id": "c_9d3e7a50b1c2486f0a5e1d37", "status": "failed", "external_url": null, "error": "YouTube did not accept the video. Check that the file is a supported video format." }
]
}
]
}
Get one post
GET /v1/partner/members/:id/posts/:pid. Same shape as one item above.
curl "https://YOUR-PULSE-URL/v1/partner/members/m_4f9c2a71e0b35d8c6a1f9e24/posts/5b0e8f1c-3a7d-4c52-9e64-0d2f7a91b8c3" \
-H "Authorization: Bearer $PULSE_KEY"
Response 200
{
"id": "5b0e8f1c-3a7d-4c52-9e64-0d2f7a91b8c3",
"status": "published",
"scheduled_at": "2026-10-09T08:00:00.000Z",
"text": "Our October workshop is open for booking.",
"networks": [
{ "network": "linkedin", "connection_id": "c_66f1a2b3c4d5e6f708192a3b", "status": "published", "external_url": "https://www.linkedin.com/feed/update/urn:li:share:7000000000000000000", "error": null }
]
}
Cancel a post
DELETE /v1/partner/members/:id/posts/:pid. Cancels the post before it goes out, and removes it. Only a post whose status is scheduled can be cancelled. Any other post returns 409 conflict. If the post is being prepared at that moment you also get 409 conflict: try again in a few seconds. If the cancel cannot be passed on, you get 502 provider_unavailable and the post stays.
curl -X DELETE "https://YOUR-PULSE-URL/v1/partner/members/m_4f9c2a71e0b35d8c6a1f9e24/posts/5b0e8f1c-3a7d-4c52-9e64-0d2f7a91b8c3" \
-H "Authorization: Bearer $PULSE_KEY"
Response 204 (no body)
(empty)
Response 409 (already gone out)
{ "error": { "code": "conflict", "message": "This post has already gone out or can no longer be cancelled." } }
Errors
Every error has the same shape. code is for your code. message is a plain sentence that you can show to a person. field names the input at fault, when there is one. Error messages never contain keys, internal ids or the name of the service behind Pulse.
{ "error": { "code": "validation", "message": "A YouTube title is at most 100 characters — this is 112." } }
| HTTP | code | When |
|---|---|---|
| 400 | confirm_required | You called DELETE on a member without ?confirm=true. |
| 400 | invalid_json | The body is not valid JSON, or is not a JSON object. An empty body counts as {}. |
| 401 | unauthorized | The key is missing, wrong or switched off. |
| 403 | forbidden_scope | Your key was used on a path outside /v1/partner. |
| 404 | not_found | The member, connection, post or endpoint does not exist, or is not yours. |
| 409 | conflict | The post cannot be cancelled now (it already went out), or it, or a post of a member you are deleting, is being prepared. Wait a few seconds and retry. |
| 409 | limit_reached | A connect session was asked for, and a limit leaves no room on any of its networks. The message names the limit. |
| 413 | body_too_large | The request body is over 1 MB. |
| 422 | validation | Something you sent is not allowed, for example too many characters, a bad date or a bad link. |
| 422 | network_not_enabled | You asked for a network that is not switched on for this member or for you, such as X. |
| 422 | not_connected | The member has no working account on a network you listed, or the account you named needs reconnecting. |
| 422 | ambiguous_account | The member has several accounts on a network and you did not name exactly one in connection_ids. |
| 429 | rate_limited | Too many requests. Wait for the time in Retry-After. |
| 500 | internal | Something went wrong on our side. Try again. |
| 502 | provider_unavailable | The posting service did not answer, or would not do what Pulse asked of it. Try again in a minute. |
400 confirm_required
{ "error": { "code": "confirm_required", "message": "Deleting a member can't be undone. Add ?confirm=true to confirm.", "field": "confirm" } }
400 invalid_json
{ "error": { "code": "invalid_json", "message": "The request body isn't valid JSON." } }
401 unauthorized
{ "error": { "code": "unauthorized", "message": "Send your partner key as 'Authorization: Bearer fnk_...'." } }
403 forbidden_scope
{ "error": { "code": "forbidden_scope", "message": "This key can only be used with the partner API (/v1/partner)." } }
404 not_found
{ "error": { "code": "not_found", "message": "No such member." } }
Other messages: "No such connection.", "No such post.", "No such endpoint."
409 conflict
{ "error": { "code": "conflict", "message": "This post has already gone out or can no longer be cancelled." } }
409 limit_reached
{ "error": { "code": "limit_reached", "message": "You've reached the limit of 3 accounts on LinkedIn." } }
413 body_too_large
{ "error": { "code": "body_too_large", "message": "That request is too large (1 MB at most)." } }
422 validation
{ "error": { "code": "validation", "message": "Instagram requires at least one media attachment." } }
When the problem is one named input, field is set, for example scheduled_at, media_urls, texts or return_url:
{ "error": { "code": "validation", "message": "scheduled_at can be at most a year ahead.", "field": "scheduled_at" } }
422 network_not_enabled
{ "error": { "code": "network_not_enabled", "message": "X is not switched on for this member.", "field": "networks" } }
422 not_connected
{ "error": { "code": "not_connected", "message": "This member hasn't connected Instagram (or it needs reconnecting).", "field": "networks" } }
422 ambiguous_account
{ "error": { "code": "ambiguous_account", "message": "This member has 2 LinkedIn accounts. Say which one with connection_ids.", "field": "networks" } }
429 rate_limited
{ "error": { "code": "rate_limited", "message": "Too many requests. Try again in 20 seconds." } }
500 internal
{ "error": { "code": "internal", "message": "Something went wrong on our side. Please try again." } }
502 provider_unavailable
{ "error": { "code": "provider_unavailable", "message": "The posting service isn't answering right now. Please try again in a minute." } }
Rate limits
Each key can make 60 requests per minute. Past that, you get 429 rate_limited and a Retry-After header with the number of seconds to wait. Requests with a missing or wrong key are not counted against any key. The connect window is not counted: a member using the window never uses up your 60 (the window has its own cap of 120 requests an hour for each link).
HTTP/1.1 429 Too Many Requests
Retry-After: 20
Content-Type: application/json
{ "error": { "code": "rate_limited", "message": "Too many requests. Try again in 20 seconds." } }
A request body can be 1 MB at most. A larger one is refused with 413 body_too_large.
Security and privacy
- No passwords, ever. Your member signs in on the network's own page. We never see or store their password.
- What we store. The member id and
external_id, the name and email you send us, a label for each connection (for example the account name), and the posts you create. We tell the posting service only an opaque label for a member, never a name or an email. - Your members are yours alone. Your key only sees your own members. An id from another partner returns
404. - Connect links. A link is signed, works for one member and the networks you named, and expires. It cannot be used to read or change anything else. A link that was changed does not work.
- Your key. We store it only in a hashed form. Keep it on your server. Your key is refused on every path outside
/v1/partner. - Links we fetch. We download your
media_urls, so they must be public https links. We refuse IP addresses, internal names, user names in links and unusual ports. - Deleting a member.
DELETE /members/:id?confirm=truecancels their scheduled posts, disconnects their accounts at the posting service and removes the member and their posts. If a post cannot be cancelled or an account cannot be disconnected, the call returns an error instead of pretending it worked, and nothing is removed. - The sign-in page. A network's own sign-in page may show the posting service's name. We do not hide it. The same note appears under the connect window.
Coming later
These are not in this version.
- Webhooks. For now, list posts and connections to check on them. The connect window's
postMessagecovers your screen. - Analytics.
- X. On request only.
- TikTok.
- Billing and usage endpoints, and a page where you create your own API keys.
Changelog
- 1.0, first release. Members, the hosted connect window, connections, limits and posts, with the error codes and rate limits described on this page.
Support
Need a key, a limit changed, or something is not working? Email fasahath@gmail.com. Tell us the request you sent and the error you got back.
The machine-readable description of this API is at /docs/openapi.json.