Networks¶
What each network can do, what it needs from you before it will work, and the things about it that surprise people.
| Network | Ready | Register the app | Post text | Pictures | Video | Schedule | Push updates | Social inbox |
|---|---|---|---|---|---|---|---|---|
| Mastodon | yes | automatic | yes | yes | yes | yes | on a timer | yes |
| Bluesky | yes | not needed | yes | yes | no | no | on a timer | yes |
| Facebook Pages | yes | by hand, reviewed | yes | yes | small only | yes | yes | no |
| YouTube | yes | by hand, reviewed | no | no | yes | yes | on a timer | no |
| yes | by hand, its own app id | no | yes | yes | no | yes | no | |
| TikTok | yes | by hand, audited | no | no | yes | no | yes | no |
| TikTok Business | yes | by hand, its own app, plus Business Center | no posting at all | no | no | no | on a timer | no |
| Threads | yes | by hand, its own app id | yes | yes | yes | no | yes | no |
| X | yes | by hand, paid plan | yes | yes | yes | no | on a timer | no |
| yes | by hand, reviewed | no | yes | yes | no | no | no | |
| Google Business Profile | yes | by hand, plus a separate API access approval | yes | yes | no | no | yes | no |
"On a timer" means the network has no way to tell us when something happens, so socialchimp checks instead. Your code gets the same updates either way.
Social inbox is 0.8.0's new surface for reading a post and its thread, replying to a comment, likes, polling for updates with a resumable marker, and direct messages — see the social inbox use case. Mastodon and Bluesky have it; everywhere else it is planned but not written yet.
Alt text¶
Media.alt_text is the description read out to somebody using a screen
reader, and it reaches Bluesky, Facebook, Instagram, Mastodon, Threads and
X — every network here that takes a picture through Media.
Two exceptions, both of them real properties of the network rather than something missing:
- Pinterest hangs alt text off the whole pin rather than off one picture,
so it is
options={"alt_text": "..."}there andMedia.alt_textis not read. A pin with five pictures has one description, which is Pinterest's own shape. - YouTube and TikTok take video and nothing else, and neither has alt text for a video.
On X it costs one extra request per file, because X has nowhere to carry a description on the upload itself.
How much of a video has to fit in memory¶
YouTube, TikTok and X send a video in pieces, reading it off disk one piece at a time, so a four gigabyte file costs one piece of memory rather than four gigabytes.
Facebook and Pinterest read the whole file first. Neither sends a video
in pieces today — Facebook's chunked upload is not written yet, and Pinterest
hands out one upload form for one request — so a video really does cost its
own size in memory on your own server while it goes out. Facebook refuses
anything over a gigabyte for that reason, and biggest_video_bytes lowers
the line if a gigabyte is more than your server has.
Mastodon¶
The easiest one to try. socialchimp registers the app for you, so there is no portal and no waiting for approval.
app = await sc.create_app(
"mastodon",
host="mastodon.social",
name="My App",
redirect_uri="https://example.com/cb",
)
Do this once per server. Every Mastodon server is separate, so an app registered on mastodon.social means nothing on fosstodon.org.
- Post options:
visibility(public,unlisted,private,direct),spoiler_text,sensitive,language - A post's numbers can be read back with
await account.read_stats(post_id): replies, favourites and boosts, ascomments,likesandshares. Mastodon publishes no reach, no impressions and no click count, so those are missing rather than zero. A post that has been deleted answersNotFoundError. - Tokens never expire. Nothing to renew.
- The post length is set by whoever runs the server — 500 by default,
5,000 on plenty of them. Read it with
await account.limits()rather than assuming. - The social inbox is all here:
account.read_post,account.read_thread,account.reply,account.like/unlike/read_likes,account.fetch_updates_after/mark_seen, andaccount.read_conversations/read_messages/send_message/mark_read/start_conversation. See the social inbox use case for working examples. - A reply to a direct or followers-only status keeps that visibility.
Otherwise nothing is sent, so the account's own default applies rather than
socialchimp forcing
public. Like.liked_atis alwaysNone. Mastodon does not say when a favourite happened, only who made it.- Direct messages are only part of the history. Mastodon has no "every
message in this conversation" call, so
read_messagesreads the/contextof the conversation's last status and keeps what it finds —Conversation.full_historyisFalseto say so, and there is no further page to ask for. pushis now in the default scopes, ready for Web Push in a later release. An account connected before 0.8.0 needs to reconnect before that lands; posting, replying and reading all keep working with "read write" alone.
Bluesky¶
No portal at all. People sign in with an app password, which is not their real password and can be revoked on its own.
start_login answers with AskForDetails rather than a link. Show the fields
it gives you, and never log the one marked secret.
Nothing has to be saved first. Bluesky is the only network here that lists
Feature.NEEDS_NO_APP, so socialchimp asks your storage for no credentials
before a sign-in and hands the platform app=None. sc.start_login("bluesky",
redirect_uri="unused") works against empty storage. There is no create_app
either — asking for one says there is no app to register, rather than sending
you to a portal that does not exist.
- Post options:
langs - Length is 300 letters and 3,000 bytes, both enforced. A family emoji is one letter and eleven bytes, so the two limits catch different posts.
- Links need marking up or they are dead text. socialchimp does this for you, including getting the byte offsets right — the most common mistake people make writing this by hand.
- Tokens last minutes and are replaced on every renewal. If you run more than one process, give socialchimp a shared lock (see getting started).
- No video yet, no scheduling.
- The social inbox is all here:
account.read_post,account.read_thread,account.reply,account.like/unlike/read_likes,account.fetch_updates_after/mark_seen, andaccount.read_conversations/read_messages/send_message/mark_read/start_conversation. See the social inbox use case for working examples. - Direct messages need an app password with DM access. Bluesky calls this
"Allow access to your direct messages" at the point you make the app
password, and it cannot be turned on for one after the fact — a new app
password has to be made with the box ticked. Without it every DM call
raises
MissingPermissionError(needs="direct messages"). - A quote's
about_post_idis the quoted post. A quote otherwise arrives asUpdateKind.MENTION— quoting is not replying, so it is folded into the same "somebody is talking about you" shape mentions already have, rather than inventing a kind of its own for one Bluesky word nothing else uses. - A marker
fetch_updates_afterormark_seendid not write raisesConfigError. Resuming from a marker that means nothing could skip updates, so passNoneto start afresh instead of guessing at one.
Facebook Pages¶
You create the app by hand, at developers.facebook.com. Meta reviews it and requires business verification before the posting permissions work at all. That review is the slowest part of getting started, so begin it early.
- It always asks which page, even when there is only one, so your app has
one code path instead of two. Handle
ChooseAccountfromfinish_loginand finish withsc.choose(...). - Post options:
link - Real scheduling, between 10 minutes and 75 days out. You get back
PostState.SCHEDULED. - A page token made from a long-lived user token does not expire.
- Webhooks work. Verify with the platform's
check_signatureon the raw bytes of the request, before anything parses them. - A video comes back
PostState.PROCESSING, notDONE. Facebook takes the bytes and carries on encoding after it answers. Ask again later withcheck_state, which reads the video'sstatusand saysDONE,PROCESSINGorFAILED. Words and pictures are live the momentpublishreturns, so there is nothing to ask about there. - A video is read into memory whole, not sent in pieces. Anything over a
gigabyte is refused with a clear message rather than half-uploaded, and
biggest_video_byteslowers that line. - Alt text works.
Media.alt_textgoes up with the picture. read_statsgives a post's numbers:likes(every kind of reaction added together),commentsandshares. A video'ssharesisNone, because there is nothing to ask a video that.fetch_updatesreads the comments on the page's latest posts (25 by default,recent_postson the constructor) asUpdateKind.COMMENT_CREATED, the same shape and the sameida webhook produces. It costs a request per post per poll, and it does not see a comment on an older post.- Reading other people's comments needs
pages_read_user_content, which is one of the permissions Meta reviews. Anybody who connected before it was asked for has to connect again. - Not written yet: who reacted, and replying to, hiding or deleting a comment.
YouTube¶
You create the app by hand in the Google Cloud console, turn on the YouTube Data API v3, and create an OAuth client. Uploading is a sensitive permission, so Google reviews it before anyone outside your test users can sign in.
- There is no text-only post. Every post is a video; a post without one is refused. Community posts are not in the API at all.
- Post options:
title(required),made_for_kids(required by Google),privacy_status,category_id,tags,notify_subscribers.Post.textbecomes the description. - A video with no
privacy_statusgoes up private, because making someone's video public by accident cannot be undone. - Shorts are not a separate thing to ask for. YouTube decides, from the shape and the length of the video.
- You get
PostState.PROCESSING, notDONE. YouTube keeps encoding after it accepts the upload. Ask again later withcheck_state. - The daily allowance is quota, not rate limiting. An upload costs about
1,600 of 10,000 units a day. Running out raises
RateLimitError, but retrying shortly is the wrong move — it resets at midnight Pacific. - Files are sent in pieces, so a large video does not have to fit in memory — unlike Facebook and Pinterest, which read the whole file first.
- No alt text. Every post here is a video, and YouTube has no alt text
for one, so
Media.alt_textis not sent anywhere.
Instagram¶
You create the app by hand at
developers.facebook.com, and add the
Instagram API setup to it. This is not the Facebook app - it is a separate
one, with its own app id and secret, sitting in the "Instagram > API setup
with Instagram login" section of the dashboard. Using the Facebook app's
credentials here gets past the sign-in page - Meta accepts the redirect - and
then fails at the token swap with a message that does not say why. Save the
Instagram pair under the platform name instagram and use that.
Unlike Facebook Pages, which sign in through Facebook Login, Instagram uses
"Business Login for Instagram" - its own sign-in page, at
instagram.com/oauth/authorize, not facebook.com. No Facebook Page is
involved anywhere, and signing in never asks which account.
- Only Business and Creator accounts can publish. A personal account never can, through any API. socialchimp says so plainly instead of letting a confusing permission error come back.
- Instagram fetches the picture itself, from a web address. It does not
accept an upload, so
Media.from_url(...)works and a local file is refused with an explanation. Put the file somewhere public first. - There is no text-only post. Every post needs a picture or a video.
- Publishing is two calls with a wait in between: build the post, wait for
Instagram to finish with it, then publish. socialchimp does the waiting, for
pictures as well as video, and for every piece of a carousel and the
carousel itself. A picture is looked at straight away and then after 1, 2,
4... seconds (never more than 30 apart); a video every minute, as Meta's
guide advises. If it runs out of patience you get a message saying the post
may still appear — because it might, and being told it failed when it
later succeeds is worse than being told we do not know. Because the waiting
happens inside
publish, a post here is live by the time you get it: you never seePostState.PROCESSING, and there is nocheck_stateto need. - "Not ready yet" (error 9007) is a
RateLimitError. Instagram can still say it after a container reportedFINISHED. Nothing was published, so socialchimp asks again twice, five seconds apart; if it is still not ready you get aRateLimitErrorwithretry_after=30, and trying the same post again is safe. - A picture with the wrong shape is an
InvalidPostError(error 36003). Feed pictures have to be between 4:5 and 1.91:1, and sending the same one again will not help. - Post options:
carousel(only needed to force a single picture into a carousel; two or more already make one). 2 to 10 items. - The daily posting limit is read, not written down. Meta's own
documentation gives three different numbers, so socialchimp asks the network
and puts the answer in
limits().posts_left_today. - Captions: 2,200 characters, up to 30 hashtags.
- No scheduling, and no deleting — neither exists in the API.
- Webhooks work: comments, mentions, live comments, story insights.
- Refresh is real, unlike the Facebook-linked flow. One request, no app
secret, and the token is good for another sixty days. Meta's own
documentation names no minimum token age before it allows this - unlike
Threads' documented 24 hours - so socialchimp uses a rule of its own rather
than guess: it only asks once 30 days or less remain, and hands back the
same token unchanged if you call
refreshearlier than that.
TikTok¶
You create the app by hand at
developers.tiktok.com, add Login Kit and the
Content Posting API to it, and add your redirect address. TikTok calls the two
values the client key and the client secret; the client key goes in
AppCredentials.client_id.
Then read the next paragraph, because it is the one people lose a day to.
- Until TikTok has audited your app, everything it posts is private. An
unaudited app may post for at most 5 people in any 24 hours, and every
single thing it posts is forced to
SELF_ONLY— visible to the account owner and to nobody else. Not their followers, not the For You page, not a friend they send the link to. You may ask forPUBLIC_TO_EVERYONE, TikTok will answer that it worked, the video will be there when the person looks at their own profile, and the rest of the world will never see it. There is no bug to find. Submit the app for TikTok's compliance audit — usually a week or two — before you promise anybody a public video, and until then tell your users their posts are private. - There are two places a post can go, and you choose.
options={"send_to": "drafts"}puts the video in the person's TikTok inbox and they finish it and publish it themselves in the app.options={"send_to": "profile"}posts straight to their profile. The drafts are the default, because they need only thevideo.uploadpermission —video.publishis a separate and harder ask — and because nothing reaches anybody's profile without a person tapping a button. Say"profile"when you mean it. - The drafts carry no caption. TikTok's inbox takes the file and nothing
else; the person writes the words themselves. So a drafts post with
Post.texton it is refused rather than having your caption quietly disappear. Clear the text, or send it to the profile. - There is no text-only post. Every post is a video; a post without one is refused.
- Post options (profile posts only):
send_to,privacy_level,disable_comment,disable_duet,disable_stitch,video_cover_timestamp_ms,brand_content_toggle,brand_organic_toggle.Post.textbecomes the caption — TikTok's API calls it the title, but it is the words under the video, and there is no second field. - A video with no
privacy_levelgoes up asSELF_ONLY, because putting someone's video in front of the world by accident cannot be undone. - The caption limit of 2,200 is not 2,200 characters. TikTok counts the way Java does, where an emoji is two, so 1,101 thumbs-up is over the line. socialchimp counts the same way TikTok does.
- You get
PostState.PROCESSINGfor a profile post, because TikTok keeps encoding and moderating after it takes the bytes — ask again later withcheck_state. A drafts post comes backPostState.WAITING_FOR_PERSON: the network has finished, and nothing else happens until somebody opens the app, so there is nothing to wait for. - Webhooks work, for
post.publish.complete,post.publish.publicly_available,post.publish.inbox_delivered(which arrives asUpdateKind.POST_DRAFTED) andpost.publish.failed. Verify with the platform'scheck_signatureon the raw bytes of the request, before anything parses them. TikTok retries for 72 hours and delivers at least once, so the same message arriving twice is normal — giveDispatcheraSeenUpdatesand the second copy is dropped for you. - Using a refresh token destroys it. TikTok hands back a new one every
time, so save both halves of what
refreshreturns. An access token lasts a day and a refresh token a year. - Files are sent in pieces, so a large video does not have to fit in memory. Up to 4 GB. Facebook and Pinterest read a whole video first instead.
- No alt text. Every post here is a video, and TikTok has no alt text for
one, so
Media.alt_textis not sent anywhere. - The daily posting cap belongs to the creator, not to your app — about 15
posts in 24 hours, shared across every app they use. It comes back as a
RateLimitError, but waiting a few seconds is the wrong move; only tomorrow helps. - No scheduling — TikTok's API has no way to ask for it.
- No deleting — there is no call for it.
- No photo carousels yet. TikTok can post up to 35 pictures, but through a different call that fetches each one from a public web address on a domain you have proved is yours. A post with pictures on it is refused with a message saying so.
TikTok Business¶
A different TikTok product from the one above, with nothing shared between
them. Posting to TikTok is tiktok, the Content Posting API, signed in
through Login Kit. Reading and answering comments is tiktok_business, the
Comment Management side of TikTok for
Business, signed in through its
own, separate portal. Different app, different client id and secret, different
sign-in page, and the account has to be linked to a TikTok Business Center
before any of it works - a manual step on TikTok's side that nothing here can
do for you.
It manages comments on ads, not on any video you like. Every write here -
replying, deleting, hiding - carries ad_id alongside the comment, straight
from TikTok's own request schema. This is TikTok's ads/business Comment
Management API; nothing about it is documented as working for a plain organic
post nobody has boosted.
- Read-only for posting.
tiktok_businesshas noFeature.*flags at all, andpost()refuses outright, by name, every time. Publish throughtiktokinstead; use this one to read and answer what happens under a post once it is up. - Comments arrive as
Updates, the same object a webhook would hand you elsewhere -fetch_updatespollscomment/listand hands backUpdateKind.COMMENT_CREATED.account.reply_to_update,account.delete_commentandaccount.set_comment_visibilityanswer, remove or hide one, reading whatever TikTok needs -ad_id,identity_id, the rest - straight off the comment's ownraw, the untouched dict TikTok sent. fetch_updatesneedsConnection.extra["advertiser_id"]andConnection.extra["ad_id"]set by hand. There is no call in this API that looks either one up for you, and TikTok'scomment/listhas no "everything for this advertiser" mode - it wants one ad named up front, so an account missing either is refused withConfigErrorrather than sent an empty search.- Not everything here comes from TikTok's own documentation. The four
comment endpoints - listing, replying, deleting, hiding - are confirmed
against TikTok's own SDK
source, field for
field. Three things are not, and are called out by name in the module's own
docstring rather than presented as fact: the endpoint
refreshuses (the SDK documents no refresh call at all, despite saying a token needs renewing daily), the exact shape of a single comment object incomment/list's reply (so field names onUpdateare a best-effort guess -Update.rawalways carries what TikTok actually sent, so nothing is lost if the guess is wrong), and the words"HIDE"/"SHOW"sent to hide or show a comment. Worth checking each against a live TikTok Business account before depending on it in production. - No video-level stats. TikTok is said to have a
business/video/listendpoint returning like/comment/share/view counts, but it appears nowhere in TikTok's own SDK - only in search-indexed fragments with no confirmed shape. Rather than ship a guessed schema, it is left out; there is noread_statshere. - Tokens: an access token from
finish_loginlasts as long as TikTok's reply says (expires_in, when present); nothing is guessed when it is not.refreshis best-effort - see above.
Threads¶
You create the app by hand at developers.facebook.com, the same as Facebook Pages — and then the thing that catches everybody:
- Adding the Threads use case makes a second app id and app secret. They
sit next to the pair the same app already uses for Facebook and Instagram,
and they are not interchangeable. Reusing the Facebook pair fails in the
worst way: the sign-in page accepts it, the person approves, and the token
swap at the end refuses under whatever code Meta feels like, with a message
that mentions none of this. Save the Threads pair with
Storage.save_appunder the platform namethreads, and socialchimp keeps the two apart for you.
Almost nothing else about it lives where the rest of Meta lives:
- Signing in is not Facebook Login. People approve at
threads.net, and the code is swapped atgraph.threads.net. The API is there too, not ongraph.facebook.com. - It never asks which account. Facebook asks which page; a Threads
sign-in is one profile, so
finish_loginfinishes rather than handing you aChooseAccount. - Renewal actually works here, unlike the Facebook-linked flow. Facebook
hands out no refresh token at all — a token is extended by trading it in
while it still works, or the person signs in again. Threads has a real
refresh endpoint: one request, no app secret, and the sixty-day clock starts
again. A job that runs once a month keeps a connection alive indefinitely.
The one rule is that a token has to be 24 hours old before it will renew
one; asking sooner raises
RateLimitErrorwith the wait onretry_after, and nothing is wrong with the token. (Instagram has a real refresh too now - see the Instagram section above - though Meta documents no minimum age for it the way it does here.) - Threads fetches the picture itself, from a web address, exactly as
Instagram does.
Media.from_url(...)works and a local file is refused with an explanation. - It does take a post of words alone, which Instagram does not.
- Publishing is two calls with a wait in between: build the post, wait for
Threads to finish with it, then publish. socialchimp does the waiting for
every container — words and a picture included, not only video, since a
container answers the request that made it before Threads may be finished
with it. A container that is already ready costs one extra look and no
waiting; a picture (or words) is then looked at again after 1, 2, 4...
seconds, never more than 30 apart, and video every 30 seconds as before. If
it runs out of patience you get a message saying the post may still
appear. As on Instagram, that means a post here is live by the time you get
it: no
PostState.PROCESSING, and nocheck_stateto need. - Unlike Instagram, "not ready" has no name here. Instagram's identical container shape can refuse publishing with a named, retryable error a moment after the container is made; nothing here confirms Threads returns the same error, or any particular one, so none is named. The waiting above should make it rare, and if Threads does refuse this way it still surfaces, as a plain error with the whole reply kept on it.
- The length limit is 500 bytes, not 500 characters. Threads' own documentation says characters and means bytes, so an emoji costs four and 500 emoji are 2,000. socialchimp counts the way Threads does.
- Post options:
carousel(only needed to force a single picture into a carousel; two or more already make one). 2 to 20 items — twice what Instagram takes. - The daily limits are read, not written down: 250 posts and 1,000
replies in a rolling 24 hours, and replies do not come out of the posts.
Posts left lands in
limits().posts_left_today;allowance()gives you both numbers. - Deleting works, which it does not on Instagram. 100 a day per account.
- No scheduling — there is no call for it.
- Replying works.
Post(text=..., reply_to=post_id)sendsreply_to_idon the top-level container — the whole post, or a carousel's parent, never one of its pieces — and it is counted against the 1,000-a-day reply allowance rather than the 250-a-day post one. See Meta's Create replies. You can only reply where you own the root post, unless the app also holdsthreads_manage_mentionsorthreads_keyword_search— neither is a default scope, so add one toscopesat sign-in to answer a mention on somebody else's post. - Reading replies back is
account.read_replies(post_id)— the top-level replies, or every depth flattened withwhole_conversation=True— andaccount.fetch_updates(since), which polls the account's latest posts instead. Both build the sameUpdateshape thereplieswebhook does, so one handler and oneSeenUpdatesanswer a reply once whichever way it arrived. See Meta's Replies and conversations. - Answering and moderating go through the same names every platform
here uses:
account.reply_to_update(update, text)publishes a reply to arepliesormentionsupdate, andaccount.set_comment_visibility(update, hidden=True)hides one —POST /{id}/manage_reply, which only works on a top-level reply and hides whatever was said back to it along with it. See Meta's Reply management.account.delete_comment(update)always refuses: Threads has no call for removing somebody else's reply, only for hiding it. - A post's own numbers:
account.read_stats(post_id)reads six metrics in one request and maps three of them —likes,repliesascomments,repostsasshares.views,quotesand the share button's own count stay onraw. See Meta's Insights. - Webhooks are narrower than the rest of Meta's: only
replies,mentions,publishanddelete, and nothing at all where a private account is involved — a reply or a mention on media owned by a private account never arrives, whatever your app subscribes to. Verify withcheck_signatureon the raw bytes, before anything parses them. See Meta's Threads webhooks. fetch_updatescosts1 + recent_postsrequests at most, every poll — one to list the account's latest posts, one per post to read its conversation.recent_postsis 25 by default; lower it if that is more than your allowance wants to spend on a timer.
Replies and numbers, worked¶
Posting a reply is an ordinary post with reply_to set:
result = await account.post(
Post(text="Thanks for the kind words!", reply_to="17900000000000000")
)
Reading the replies to one post — top-level only, or the whole thread:
top_level = await account.read_replies("17900000000000000")
whole_thread = await account.read_replies("17900000000000000", whole_conversation=True)
Polling an account for what is new, the way Poller calls it, costs one
request to list the account's recent posts and one per post:
Answering one, and hiding one that should not be public:
await account.reply_to_update(update, "Glad you liked it!")
await account.set_comment_visibility(update, hidden=True)
A post's numbers, with the three unmodelled metrics read off raw:
stats = await account.read_stats("17900000000000000")
print(stats.likes, stats.comments, stats.shares) # likes, replies, reposts
by_name = {entry["name"]: entry for entry in stats.raw["data"]}
views = by_name.get("views")
quotes = by_name.get("quotes")
share_button = by_name.get("shares") # the button, not the "reposts" metric
What a replies webhook looks like, and what socialchimp makes of it:
{
"app_id": "123456",
"topic": "moderate",
"target_id": "78901",
"time": 1723226877,
"subscription_id": "234567",
"has_uid_field": false,
"values": {
"field": "replies",
"value": {
"id": "8901234",
"username": "test_username",
"text": "Reply",
"media_type": "TEXT_POST",
"permalink": "https://www.threads.net/@test_username/post/Pp",
"replied_to": {"id": "567890"},
"root_post": {"id": "123456", "owner_id": "123456"},
"shortcode": "Pp",
"timestamp": "2024-08-07T10:33:16+0000"
}
}
}
update.id is "78901:replies:8901234" — the account, the field and the
reply's own id, so the same reply pushed twice makes the same update.
update.raw is exactly the value object above, whether that update came
from this webhook, read_replies, or fetch_updates — so a handler reads
update.raw["text"] the same way regardless of how the reply was heard
about.
X¶
Posting costs money, and there is no fixed price to write down. Access to
the API that lets an app publish is paid and tiered, what a plan includes
changes without notice, and any number put here would be out of date within
months. When your plan does not cover something, X does not say "you have
not paid" — it answers 403 with client-not-enrolled, which reads exactly
like a scope your app forgot to ask for, and people spend afternoons
rewriting scopes over it. socialchimp names this one on sight: the message
says plainly that the plan refused this, not your code, and points at
console.x.com, where you look up and change what
your app is allowed to do.
You create the app and its OAuth client by hand, at
developer.x.com. There is no
create_app here — somebody has to fill in the form, agree to the terms,
choose a plan, and add your redirect address to the OAuth client themselves.
- Ask for
offline.accessor tokens die in two hours. Leave it out of your scopes and X sends back no refresh token at all — the access token stops working two hours later and the person has to sign in again. It works perfectly the morning you write it and starts logging people out after lunch. - Post options:
reply_settings(everyone,mentionedUsers,following,subscribers),quote_tweet_id. - The limit is 280 characters, counted the way JavaScript counts them — an emoji is two. X's paid subscribers can post longer ones, and nothing in the API says whether the account we are posting as is one of them, so 280 is what socialchimp checks for everybody; a subscriber's own longer post still gets through by asking for the limit they know they have.
- A thread is posts chained together, not one thing X has.
publish_threadsends each one pointed at the id before it. If one fails partway through, nothing already published is deleted and nothing after it is sent — you get aPartialThreadErrornaming how far it got, so you can carry the thread on from the last id once the problem is fixed. - Files go up the old way: INIT, then APPEND once per piece, then FINALIZE, and for video a STATUS call in a loop until X finishes encoding it. Sent a piece at a time off disk, so a large video does not have to fit in memory.
- Alt text is a request of its own. X has nowhere to carry a description
on the upload, so a
Media.alt_textgoes up afterwards throughPOST /2/media/metadata, before the file is named on a post — X will not take one for a file that is already published. A file with no alt text costs no extra request. - Deleting works.
- No scheduling.
Feature.SCHEDULEis missing, so a post withpublish_atis refused rather than published now. - No app to create. See above.
- No pushed updates. X's streaming and account-activity products are
both behind paid plans of their own, so mentions are read on a timer
through
fetch_updatesinstead, which works on every plan that can read at all.
Pinterest¶
Every pin needs a board. Pinterest has no feed to post to — there is no
such thing as a pin without one — and socialchimp never chooses one for you:
name board_id on the post, or save one on the connection's extra if your
app has a sensible default. A pin naming neither is refused before anything
is sent, with a message naming both routes. boards(connection) lists what
an account has, so you can build a picker.
A new app gets Trial access, and its pins are visible only to you. Your code runs against the real API, with real credentials, and gets back real 2xx replies carrying real pin ids — and nobody but the person who made them can see the pins. Not the public profile, not anybody's home feed, not a friend looking at the account. Nothing anywhere says this is happening, so the first thing to check when a pin "did not appear" is whether the app is still on Trial. Getting to Standard access is a review: a privacy policy and a video recording of your app taking a real person through the real sign-in. Check the access tiers page — there is no field anywhere in the API that says which tier you are on.
You create the app by hand at developers.pinterest.com/apps.
- There is no PKCE. Pinterest's v5 API refuses a
code_challengerather than accepting it, soSendToNetwork.remembercomes back empty here — that is a real property of Pinterest, not something missing. - Creating a pin needs all four scopes:
boards:read,boards:write,pins:read,pins:write. Asking for only thepins:pair gets a 403 on the first pin, which reads like a problem with the board rather than a permission never asked for. - Post options:
board_id,board_section_id,title,link,alt_text,dominant_color. - Alt text belongs to the pin, not to one picture. It is
options={"alt_text": ...}here, andMedia.alt_textis not read — a pin with five pictures has one description, which is Pinterest's own shape rather than something missing. Post.textis the pin's description, andtitleis a separate setting — the thing people trip over. The description takes 800 characters, the title 100.- Tokens last 30 days; the refresh token lasts 60 and is replaced every
time you use one. An account nobody has posted from in two months needs
signing in again, because its refresh token ran out before anything
renewed it. That day is on
Token.refresh_token_expires_at, so you can put a "reconnect Pinterest" prompt in front of somebody before a post fails rather than after. - Pinterest really will fetch a picture from a web address.
Media.from_url(...)costs nothing here. Two to five pictures become one pin people can swipe through — all files or all links, never a mixture. - Video is three steps and a different server: registered with Pinterest, uploaded to Amazon with a form Pinterest hands you (no account token goes with it — the upload is not going to Pinterest), then waited for. Pinterest gives out one form for one upload, so the whole file goes in a single request and really does cost its own size in memory. Facebook is the same; YouTube, TikTok and X send theirs in pieces.
- Deleting works.
- No text-only pin. Every pin needs a picture or a video;
Feature.POST_TEXTis off and a post with nothing attached is refused. - No comments at all, so no replies. There is no comment endpoint
anywhere in v5; a post with
reply_tois refused by name. - No scheduling.
Feature.SCHEDULEis missing. - No updates worth having. No webhooks for ordinary pins, and nothing in
the API reports that something happened — so there is no
fetch_updateshere andFeature.PUSH_UPDATESis off.
Google Business Profile¶
A place, not a feed. Signing in gets you a location - one business's listing - and posting is the smallest part of what there is to do with it: a location also has a name, a phone number, an address, a category, and a verification process with nothing to do with signing in at all.
You create the app by hand, and then apply separately. Make a project at console.cloud.google.com, turn on the Business Profile APIs, create an OAuth client - and then apply for Business Profile API access through Google's own contact form, a manual approval on top of the OAuth client that can take weeks. An unapproved project's quota on these APIs is zero, whatever the OAuth client says, and nothing in the API tells you which side of that approval you are on.
- It always asks which location, even when there is only one, the same
rule every network here holds to. One Google account can manage several
businesses, and each business several locations -
finish_loginlists every one of them and hands youChooseAccount. - Google fetches the picture itself, from a web address, the way Facebook
and Instagram do.
Media.from_url(...)works; a local file is refused. One picture at a time. - Post options:
call_to_action_type(BOOK,ORDER,SHOP,LEARN_MORE,SIGN_UP,CALL),call_to_action_url(needed for every type butCALL). - Only ordinary posts are written here. Google's API also has event posts and offer posts, each with their own extra fields; neither is written yet, and asking for one is refused by name.
- No
read_stats. Google's Performance API reports how the location is doing in search and Maps, not how one post did - there is no honest number to hand back for a post id, so this is left off rather than approximated. - Reviews and questions are answered, not just read - the first network
here that needs it.
await account.reply_to_update(update, "Thank you!")answers either one;reply_to_updatereadsupdate.kindto work out which.CanReplyToUpdates, onsocialchimp.platform, is the shape a platform provides to say it can do this. -
Updates arrive through Cloud Pub/Sub, not a plain webhook. Every other pushing network here signs a request with a shared secret checked by plain HMAC, entirely offline. Google's Pub/Sub instead signs with a Google-issued OIDC token whose signature can only really be checked against Google's own rotating public keys - fetching them on every check would be a network call inside what is supposed to be a cheap one, so this platform does not fetch them itself.
secrethere is a small JSON document your app keeps refreshed instead of a password:{ "keys": [
], "audience": "https://you.example/webhooks/google_business", "service_account": "service-1234@gcp-sa-pubsub.iam.gserviceaccount.com" }
Refresh keys from Google's own JWKS endpoint on a timer - they last hours,
not minutes. Audience alone is not proof of anything, since Google will
sign a token for any audience a caller names - service_account is the
identity your Pub/Sub push subscription actually authenticates as, and
check_signature checks the token's email claim against it, the way
Google's own push documentation says to. fetch_updates still works too,
polling reviews and questions on a timer, for an app that cannot receive a
push at all.
- Business information can be read and changed:
await account.get_location() and
await account.update_location({"title": "New Name"}). fields names
only what changes, the way Google's own field mask does.
CanEditBusinessInfo is the shape a platform provides for this.
- Verification never sees the proof.
await account.start_verification(method) is what makes Google act - mail
a postcard, place a call, send a text or an email - to the business's own
address, phone or inbox. await account.complete_verification(id, pin)
takes the code the business owner was sent and nothing else; socialchimp
never generates, stores or sees it otherwise. CanManageVerification is
the shape a platform provides for the whole process.
- No scheduling, and no check_state. A post goes out when you publish
it, and is live by the time Google answers - only a rejected one is
reported as anything other than done.
A network that is not here¶
You do not have to wait for us. Write it yourself and publish it — socialchimp will find it. See adding a platform.