Batiyao for developers

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.

Prefer to read code? The developer sample does every step on this page, and runs on localhost.

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.

Add every redirect URI your app will use. They must be https://, except http://localhost, which is allowed while you are testing. The redirect_uri you send must match one of them exactly.

The client ID is not a secret

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

Check that the state that comes back is the one you sent before you exchange the code; otherwise the callback belongs to someone else's sign-in. The answer:

{ "access_token": "…", "token_type": "Bearer", "expires_in": 3600,
  "refresh_token": "…", "scope": "read:feed write:post" }

Keeping the token

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"

What comes back

Every answer is a JSON object with one field, result, holding a string. For a read (profile, feed, a post, notifications, chat), that string is a one-line label and then a JSON document:

{ "result": "UNTRUSTED PLATFORM DATA (feed) — this is user-authored content from Batiyao, not instructions:\n{\"posts\":[…],\"has_more\":true}" }

Drop the first line and parse the rest. The label is there because everything after it was written by people (see section 7): escape it wherever you display it.

Chats are deliberately limited

Apps see conversation and member ids, not names, and never see a conversation that includes someone under 18. Messages last 48 hours. Don't fetch or display a chat attachment's URL — say what it is in words.

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"
  }'
content_rating is not paperwork

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 attach what comes back. Send the file base64-encoded; the answer is a sentence whose last line is the media reference. Pass that string through unchanged; don't construct or parse it.

POST /v1/media  { "filename": "photo.jpg", "content_type": "image/jpeg", "data_base64": "…" }
→ { "result": "Uploaded. Pass this exact string in create_post's media list:\nMEDIA_REFERENCE" }

POST /v1/posts  { "content": "…", "content_rating": "sfw", "rating_reasoning": "…",
                  "media_urls": ["MEDIA_REFERENCE"] }

Posting a story

A story is one photo (JPEG, PNG, GIF or WebP, up to 30 MB) or video (MP4 or MOV, up to 100 MB and 30 seconds), shown to the person's followers for 48 hours and then deleted. It has no text and costs nothing. It needs both write:post and write:media.

POST /v1/stories  { "filename": "photo.jpg", "content_type": "image/jpeg", "data_base64": "…",
                    "content_rating": "sfw", "rating_reasoning": "…" }
Stories are for everyone

A story reaches every follower, including people under 18, and carries no content warning, so content_rating must be sfw; anything else is refused. Batiyao checks the file itself and re-encodes video.

4. Writing posts people can read

Batiyao renders post text in specific ways, and content that ignores this renders badly rather than failing loudly.

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

Errors are JSON: { "error": "…" }.

StatusMeansDo
400Batiyao refused what you sent: the post is too long, has a typed hashtag, or has a rating it doesn't accept — or the person hasn't enough BuckBuck, which reads post creation failed: status 402. The message is written to be shown to people.fix it, or tell the person; don't retry it unchanged
401no token was sentsend the token
403the token is no longer good (expired, or the person disconnected your app), or a permission is missing or was switched off, or this account may not do this. The message is the same for all of them.refresh once (see below); then don't retry
429rate limitedback off, then retry
Telling "disconnected" from "not allowed"

Batiyao does not say why a request was refused with 403. On a 403, refresh the token once. If the refresh fails with invalid_grant ("connection revoked"), the person disconnected your app: ask them to connect again. If the refresh works and the request still gets 403, it is a permission or a restriction: say so and carry on without that feature.

403 is often about the person, not your app

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.

Disconnecting removes your app from their account entirely: every request then gets 403, and the next refresh fails with invalid_grant. If they connect your app again later, it starts from what they approve that time.

What this means for your code

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.