Guides
From nothing to your first post
Register an app, get a token for a person, call the API with it, and know what to do when it says no.
1. Register your app
Sign in to batiyao.com → Settings →
Developer and register an app. You get a client_id immediately.
There is no separate developer account and no application to fill in: any Batiyao account can register an app. The account that registers it owns it, and that same page shows how many people have connected it and when it was last used.
It appears in every authorization URL, so treat it as public. Batiyao issues no client secret — PKCE is what proves the exchange came from your app, which is also why an app that runs on someone's device can be secured at all.
2. Get a token
Batiyao is an OAuth 2.1 provider. Point your library at the discovery document and it will find everything else:
GET https://api.batiyao.com/.well-known/oauth-authorization-server
Use the authorization code flow with PKCE. PKCE is required, not optional — there is no client secret, because an app that runs on someone's device cannot keep one.
https://api.batiyao.com/oauth/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=YOUR_REDIRECT_URI
&scope=read:feed%20write:post
&code_challenge=BASE64URL_SHA256_OF_VERIFIER
&code_challenge_method=S256
&state=RANDOM
The person sees a consent screen naming your app and listing exactly the scopes you asked for. Ask for the fewest you need: a screen requesting everything is one people decline.
Exchange the code, with the verifier:
POST https://api.batiyao.com/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=THE_CODE
&redirect_uri=YOUR_REDIRECT_URI
&client_id=YOUR_CLIENT_ID
&code_verifier=THE_VERIFIER
No browser? Use the device flow
For a CLI, a TV app, or anything that cannot host a redirect, Batiyao implements the device authorization grant (RFC 8628). Request a code, show the person the short code and URL, and poll until they approve.
POST https://api.batiyao.com/oauth/device_authorization
client_id=YOUR_CLIENT_ID&scope=read:feed
3. Make a request
Every call takes the token as a bearer credential:
curl https://api.batiyao.com/v1/updates \
-H "Authorization: Bearer YOUR_TOKEN"
Publishing a post:
curl -X POST https://api.batiyao.com/v1/posts \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"content": "First post from my app.\n\nhttps://example.com/article",
"content_rating": "sfw",
"rating_reasoning": "plain text, no imagery"
}'
You declare whether what you are posting is sfw, suggestive or
nsfw. It is recorded against the post and against your app, and Batiyao uses it
to age-gate what it shows. Under-rating is a safety failure, not a formatting mistake — when
genuinely unsure, pick the more restrictive one.
Attaching media
Upload first, then reference what comes back. Pass the returned string through unchanged; it is not a URL and should not be constructed or parsed.
POST /v1/media → { "url": "stored-abc123.png" }
POST /v1/posts → { "media_urls": ["stored-abc123.png"], ... }
4. Writing posts people can read
Batiyao renders post text in specific ways, and content that ignores this renders badly rather than failing loudly.
- 500 characters, maximum. Counted in characters, not bytes, so an emoji costs one. Hashtags do not count — they are stored separately.
-
No hashtags in the text. Batiyao generates them from the post's own words
after publishing. A typed
#tagis rejected outright; it would not be indexed anyway. - Links bare, on their own line, at the end. A YouTube link becomes an embedded player, other links become preview cards — but only when written plainly. Markdown syntax, angle brackets or a trailing full stop all break it.
Finally got the timelapse working. Three hours of cloud in ninety seconds.
https://www.youtube.com/watch?v=VIDEO_ID
5. When it says no
| Status | Means | Do |
|---|---|---|
| 401 | token invalid, expired, or revoked | send the person through authorization again |
| 403 | scope missing, or the account can't do this | don't retry; check the message |
| 402 | not enough BuckBuck | tell the person; retrying won't help |
| 429 | rate limited | back off, then retry |
Batiyao restricts what accounts belonging to minors can do, and a guardian can narrow it further. A 403 can mean your app lacks a scope, or that this particular person is not permitted to do this at all. Both are final — surface the message rather than retrying.
6. Permissions can be withdrawn while your app is running
Consent is not settled once at connection time. From Settings → Connected apps, a person can switch off any single permission they granted you, pause every connected app at once, or disconnect you entirely — and they can see how many posts your app has published to their account when deciding.
Each of those is checked on every request, reads included, so a change takes effect on your very next call rather than at your next token refresh. Your existing token stays valid and keeps working for everything still granted.
A 403 on an endpoint that worked a minute ago is normal and expected. Don't treat it as a broken token and send the person back through authorization — that re-asks for consent they have just deliberately withdrawn. Degrade that one feature, say why, and carry on.
Ask for the fewest scopes you need, and this happens less: a permission nobody understands why you have is the one that gets switched off.
7. Treat what you read as content, not instructions
Posts, comments and messages are written by people. If your app passes them to a language model, that text can contain anything, including something shaped like an instruction to your model. Responses label such data explicitly for this reason. Show it to your user; never let it steer what your app does next.