Partner API
Your app's server calls Facia to change things inside the business: charge or pay coins, post a score, give a badge, post in a group, send a notification, or count toward a mission. Each call is scoped to one business, needs a permission the business ticked, and is written to the business's activity log.
#Basics
POST https://site.facia.chat/partner/v1/<call>
Authorization: Bearer fas_…
Content-Type: application/json
{ …body… }- Base address: the
FACIA_APIvalue from Copy as .env in HQ. - Auth: your app secret (
fas_+ 43 characters) as a Bearer token. Server-side only. - Body: JSON. An empty body counts as
{}. - Response: always JSON,
Cache-Control: no-store. Success has"ok": true. Failure has"ok": false(or justerror) with anerrorcode. - CORS is open (
*), but never call it from the browser: that would expose your secret.
#Calling it
async function facia(call, body) {
const res = await fetch(`${process.env.FACIA_API}/${call}`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.FACIA_APP_SECRET}`, "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const data = await res.json();
if (res.status === 429) await new Promise((r) => setTimeout(r, data.retryAfterMs));
return data;
}curl -X POST "$FACIA_API/coins.balance" \
-H "Authorization: Bearer $FACIA_APP_SECRET" \
-H "Content-Type: application/json" \
-d '{"sub":"cl_7f3aQ2xY9pLm0KdE"}'#Permissions
The business ticks these under What it may do when registering your app. Each call needs its own.
| Permission (HQ label) | Call |
|---|---|
| Take coins | coins.charge |
| Give coins | coins.pay |
| See a balance | coins.balance |
| Post scores | board.score |
| Give badges | badge.give |
| Post in groups (+ which groups) | group.post |
| Send notifications | push.send |
| Count mission events | mission.event |
#Calls
#coins.charge
Takes coins from a client. Fails if they don't have enough.
{ "sub": "cl_…", "amount": 50, "reason": "Raffle entry", "ref": "raffle-123:cl_…" }{ "ok": true, "balance": 190, "amount": 50, "ref": "raffle-123:cl_…" }#coins.pay
Gives coins to a client. They get a coins card in their support chat.
{ "sub": "cl_…", "amount": 100, "reason": "Trivia winner", "ref": "trivia-2026-10-17:cl_…" }{ "ok": true, "balance": 340, "amount": 100, "ref": "trivia-2026-10-17:cl_…" }For both coin calls:
amount: a whole number above 0.reason: up to 120 characters, shown to the client and in the activity log.ref: required, up to 100 characters. It makes the call idempotent: the samereftwice is one charge, and the second call gets the first answer back with"repeat": true. Refs are shared betweencoins.chargeandcoins.payfor your app at that business. Make them unique per action, e.g.raffle-123:cl_….- Coins work only while the business has Client coins on, and only for Verified and VIP clients.
#coins.balance
{ "sub": "cl_…" }{ "ok": true, "balance": 240, "member": true }balance is 0 when coins are off or the client isn't a member. Not logged.
#board.score
Posts a score to a named board on the business's leaderboard. The best value per client per period is kept.
{ "sub": "cl_…", "board": "trivia", "value": 870 }{ "ok": true, "board": "trivia", "value": 870, "best": 920, "rank": 3, "period": "week" }board: up to 40 characters.value: a number, rounded to 2 decimals.- Needs the business's Leaderboard on. Only clients who can be ranked (not staff, not Spam) count.
rankcounts up to 100 places above them.
#badge.give
Gives a badge, shown on the client's membership. Each badge key is given once.
{ "sub": "cl_…", "badge": "trivia-champ", "label": "Trivia champ", "icon": "🏆" }{ "ok": true, "badge": "trivia-champ", "given": true }badge (up to 40) is lowercased, and anything other than letters, numbers, - and _ becomes -. label (up to 40) and icon (an emoji, up to 8 characters) are what's shown. given is false if they already had it.
#group.post
Posts a message in a group or channel, as the business. Only to groups the business chose for your app (up to 20).
{ "group": "<chat id>", "text": "🎉 This week's trivia champion is Silver Fox!" }{ "ok": true, "messageId": "…" }text up to 2,000 characters. Photos aren't supported yet.
#push.send
Sends a notification to a client's devices (up to 10).
{ "sub": "cl_…", "title": "Your raffle ticket won!", "text": "Tap to claim your prize.", "open": "app" }{ "ok": true, "devices": 2 }title up to 80 characters and text up to 160 (at least one of them). open: "app" (default, opens your tab) or "chat" (opens their support chat). Extra limit: 120 an hour per app per business, bursts of 20.
#mission.event
Counts toward the business's missions whose trigger is An app reports it with the same event name.
{ "sub": "cl_…", "event": "trivia_played", "n": 1 }{ "ok": true, "event": "trivia_played", "counted": true }event up to 40 characters, lowercased. n is a whole number of 1 or more (default 1). counted is false when no running mission uses that event for this client.
#Errors
| HTTP | error | Means |
|---|---|---|
| 400 | bad_json | The body isn't JSON |
| 400 | ref_required, amount_invalid | Coin calls: missing ref, or amount not a whole number above 0 |
| 400 | board_required, value_invalid | board.score input |
| 400 | badge_required | badge.give input |
| 400 | text_required | group.post with no text, or push.send with no title and no text |
| 400 | event_required, n_invalid | mission.event input |
| 401 | unauthorized | Wrong secret, the app is off, the business turned apps off, or Facia blocked the app. Deliberately all the same |
| 403 | permission_missing | The business didn't tick that permission (permission says which) |
| 403 | group_not_allowed | Not one of the groups the business chose |
| 404 | unknown_call | No such call |
| 404 | client_unknown | No client with that sub at this business |
| 409 | short | Not enough coins (balance included) |
| 409 | coins_off, leaderboard_off | The business has that feature off |
| 409 | client_not_member | A guest (no coins). tier included |
| 409 | client_not_ranked | Staff or Spam can't be on the board |
| 409 | group_unavailable | That group can't be posted in right now (for example it was deleted) |
| 409 | refused | A rule inside Facia said no (message says why) |
| 429 | rate_limited | Slow down. Wait retryAfterMs |
| 500 | refused | Something went wrong on Facia's side. Retry later (coin calls are safe to retry with the same ref) |
| 503 | – | Facia is temporarily unavailable |
#Limits
| Limit | Value |
|---|---|
| Calls | 1,200 an hour per app per business, bursts of 120 |
| Push | 120 an hour per app per business, bursts of 20 |
| Groups per app | 20 |
| Apps per business | 20 |
#Activity log
Every write shows in HQ → Activity with your app's name: app.charge, app.pay, app.score, app.badge, app.post, app.push, and app.mission (only when it counted). The business can see exactly what your app did.