Friends and teams
Friend codes, requests, blocks, nudges and high-fives, friend standings, friend quests, friend streaks, teams and team quests, with the privacy rules and what anonymous sessions can do.
Social features let players see and help each other: add friends by code, nudge or high-five them, compare league and leaderboard scores, share a weekly friend quest or a daily friend streak, and join a team with a shared team quest. Everything a player does goes through their customer session (publishable key plus customer token); your backend and operator tools use the operator routes.
Operator routes need customers:read or customers:write for friends, blocks and teams, and
programs:read or programs:write for friend quest, friend streak and team quest definitions.
Customer routes need the session capabilities customer:read and customer:write. Customer
social writes are naturally idempotent (a repeat returns the current state with replayed), so
they take no Idempotency-Key.
Privacy rules
- No customer id ever reaches another player. Other players are named by their friend code (a 10 character handle) and shown by display name, or by a stable pseudonym such as "Player 4821" when they have none.
- Profile visibility.
PATCH /v1/customer/social_profilesetsdisplay_name,avatar_urlandvisibility:public,friends(the name is shown only to accepted friends; everyone else sees the pseudonym) orhidden(always the pseudonym). Display names are 1 to 40 characters, with no URLs or@. An operator can hide a profile withPOST /v1/customers/{id}/social_profile_moderations. - Blocking wins everywhere. When either side blocked the other there is no request, no
friendship (a block removes it), no nudge or high-five, no friend quest or streak pairing (an
active one is cancelled or ended), and neither appears in the other's lists or friend standings.
Messages already queued about the other player are suppressed with
blocked. - Anonymous sessions have no social features. Every social read and write answers
403 forbiddenwithdetails.reasonsocial_unavailable_for_anonymous. Anonymous players still take part in leagues under a pseudonym. Sign the player in (and merge the anonymous customer) to unlock friends and teams. - Nudge and high-five
contextis a short machine key (lesson:12,streak), never free text, so players cannot message each other through it.
Friends
| Action | Route |
|---|---|
| Read my friend code | GET /v1/customer/friend_code |
| Rotate my friend code | POST /v1/customer/friend_code |
| Send a request by code | POST /v1/customer/friend_requests with { "code" } |
| Accept or decline (by the friend's code) | POST /v1/customer/friend_requests/{friend}/accept, POST /v1/customer/friend_requests/{friend}/decline |
| List friends, incoming, outgoing | GET /v1/customer/friends?status=accepted (incoming, outgoing) |
| Remove a friend | DELETE /v1/customer/friends/{code} |
| Block a player | POST /v1/customer/blocks with { "code" } |
| List my blocks | GET /v1/customer/blocks |
| Unblock | DELETE /v1/customer/blocks/{handle} |
A request to a player who already requested you becomes a friendship at once. A player has at most
200 friends and pending requests together; past that the answer is 409 invalid_state with
details.reason limit_reached. A blocked or deleted player is refused the same way
(details.reason blocked, customer_deleted), and your own code is 400 validation_failed.
Blocks are listed by handle. GET /v1/customer/blocks returns
{ "data": [{ "handle": "blk_...", "display_name", "created_at" }], "has_more" }, newest first
(at most 200). A handle is derived for this viewer only: it is not a customer id and does not work
for anyone else. DELETE /v1/customer/blocks/{handle} lifts the block (a second delete of the
same handle is 404); the blocked player's current friend code also works in place of the handle
and is safe to repeat.
Nudges and high-fives. POST /v1/customer/nudges and POST /v1/customer/high_fives with
{ "code", "context"? }: friends only, one of each kind per sender, recipient and day (a repeat the
same day returns the first with replayed: true). The recipient gets a friend.nudged or
friend.high_fived event, which can become a push or email
(Notifications). GET /v1/customer/reactions lists sent and
received ones.
Friend standings. GET /v1/customer/leagues/{id}/friend_standings ranks the player and their
friends by this period's league score across cohorts; GET /v1/customer/leaderboards/{id}/friend_standings
does the same over a leaderboard program's current board.
Your backend can import an existing friend graph with POST /v1/customers/{id}/friendships
({ "other_customer_id", "status": "accepted" }) and remove one with
DELETE /v1/customers/{id}/friendships/{other}; it reads GET /v1/customers/{id}/friends and
GET /v1/customers/{id}/blocks.
Friend quests
Two friends share one target for the week (or day): "complete 20 lessons together".
{
"key": "weekly_duo",
"name": "Weekly duo quest",
"config": {
"schema": "friend_quest/v1",
"period": { "kind": "week", "start_weekday": 1, "timezone": "environment" },
"target": 20,
"reward": [
{ "currency": "xp", "amount": 50 },
{ "type": "item", "item": "chest_small", "quantity": 1 }
]
}
}Create with POST /v1/friend_quest_definitions, then activate, pause, resume or archive with
POST /v1/friend_quest_definitions/{id}/activate (and /pause, /resume, /archive), each
{ "expected_revision" }. The reward list has 1 to 5 entries, currency or item.
- A player starts the quest with a friend:
POST /v1/customer/friend_questswith{ "definition": "<definition id or key>", "friend_code" }. A player is in at most one pair per definition and period. A player whose experiment variant withholds this friend quest is refused (403 forbidden,details.reasonwithheld_by_experiment). - Progress comes from a rule action
friend_quest.contribute(frontend-only apps) or from your backend:POST /v1/friend_quest_definitions/{id}/contributionswith{ "customer_id", "amount", "dedupe_key" }. - When the two sides together reach the target, the quest completes and both players are paid,
exactly once each, and both get
friend_quest.completed. A quest that is not complete by the end of its period expires; blocking or unfriending cancels it with no reward. - Reads:
GET /v1/customer/friend_quests(each definition with this period's quest, both sides' progress and the friend's display name), and for operatorsGET /v1/friend_quest_definitions/{id}/quests.
Friend streaks
A pair's day counts when both friends were active that day; the streak grows by one per such day and resets when a day is missed.
{
"key": "duo_streak",
"name": "Friend streak",
"config": {
"schema": "friend_streak/v1",
"timezone": "environment",
"milestones": [
{ "length": 7, "reward": [{ "type": "item", "item": "streak_freeze", "quantity": 1 }] },
{ "length": 30, "reward": [{ "currency": "xp", "amount": 300 }] }
]
}
}Create with POST /v1/friend_streak_definitions and manage it like a friend quest definition.
Activity comes from a rule action friend_streak.activity or from your backend:
POST /v1/friend_streak_definitions/{id}/activities with { "customer_id", "at"? }
(idempotent per day). Every friend who was also active that day extends the pair's streak.
Milestone rewards are paid once per pair, length and player. After 18:00 in the streak's zone a
pair with a streak of 2 or more where one friend has not played yet gets friend_streak.at_risk.
GET /v1/customer/friend_streaks lists the player's streaks with length, longest,
extended_today, at_risk and the next milestone.
Teams and team quests
A player is in at most one team. Teams have a name, a join_policy (open, invite or
closed) and a capacity (2 to 500, default 30).
| Action | Route |
|---|---|
| My team, role and members | GET /v1/customer/team |
| Create a team | POST /v1/customer/teams with { "name", "join_policy"?, "capacity"? } |
| Find open teams | GET /v1/customer/teams?query=<name prefix> |
| Join | POST /v1/customer/team_memberships with { "team_id" } (open) or { "invite_code" } |
| Leave | DELETE /v1/customer/team_membership |
| Edit, new invite code | PATCH /v1/customer/team, POST /v1/customer/team/invite_codes |
| Kick or change a role | POST /v1/customer/team/members/{handle}/kick, POST /v1/customer/team/members/{handle}/role |
Members are named by a per-team handle, never a customer id. The owner and admins manage the
team; an owner who leaves passes ownership on, and the last member leaving disbands it. Blocks do
not apply inside teams. Operators read GET /v1/teams, GET /v1/teams/{id} and
GET /v1/teams/{id}/members, hide an offensive name with POST /v1/teams/{id}/moderations
(shown as "Team " and 4 digits) and remove a member with POST /v1/teams/{id}/member_removals.
A team quest gives every team the same target for the period:
{
"key": "team_week",
"name": "Team week",
"config": {
"schema": "team_quest/v1",
"period": { "kind": "week", "start_weekday": 1, "timezone": "environment" },
"target": 500,
"reward": [{ "currency": "xp", "amount": 100 }],
"min_contribution": 10
}
}Progress comes from the rule action team_quest.contribute or from
POST /v1/team_quest_definitions/{id}/contributions ({ "customer_id", "amount", "dedupe_key" }).
When a team reaches the target, every current member who contributed at least min_contribution
is paid once and gets team_quest.completed. A member withheld by an experiment that targets this
team quest is recorded but paid nothing. Reads: GET /v1/customer/team/quests,
GET /v1/customer/team_quest_boards/{definition} (teams ranked by progress), and for operators
GET /v1/team_quest_definitions/{id}/board and GET /v1/team_quests/{id}/contributions.
Pausing
Pausing a friend quest, friend streak or team quest definition freezes it. While it is paused
nothing starts, no progress or activity is recorded, and nothing expires or breaks; rewards already
earned are still paid. When you resume, every quest that was running when the pause began gets its
end moved later by the time the definition was paused, so players keep the time they had left. For
friend streaks, every day the pause touched (in the streak's time zone) is excused: it neither
breaks the streak nor counts toward it, so the streak continues where it was. The customer reads
(GET /v1/customer/friend_quests, GET /v1/customer/friend_streaks,
GET /v1/customer/team/quests) keep showing paused definitions with paused: true.
Experiments
Friend quest and team quest definitions can be experiment targets,
named by key: { "type": "friend_quest", "key": "weekly_duo" } and
{ "type": "team_quest", "key": "team_week" }.
Changing definitions through change sets
In a live environment without auto autonomy an agent proposes definition changes as change-set commands
(POST /v1/change_sets): friend_quest_definition.create, friend_streak_definition.create and
team_quest_definition.create take the create body; .update takes key, expected_version and
the PATCH fields; .activate, .pause, .resume and .archive take key and
expected_version. Archiving is irreversible.
Feeding social mechanics from a frontend-only app
Rules can drive all three from customer events, so an app with no server of its own (Firebase, Supabase, React Native) needs only the publishable key and a customer session:
{
"schema": "orishare.rule/v2",
"trigger": { "kind": "customer_event", "event": "lesson.completed" },
"actions": [
{ "type": "friend_streak.activity", "friend_streak": "duo_streak" },
{ "type": "friend_quest.contribute", "friend_quest": "weekly_duo", "amount": 1 },
{ "type": "team_quest.contribute", "team_quest": "team_week", "amount": 1 }
]
}The event id is the contribution's dedupe key, so a retried event never counts twice. A player
without a friend quest or a team, an anonymous player and an inactive definition are skipped, not
errors. A browser event (customer_claim) cannot feed a definition that pays a monetary
currency (the environment's monetary_currencies, or every currency when the
trust policy lists none): pay those definitions in items or in a
non-monetary currency such as XP, and list your real-money currencies with
PATCH /v1/trust_policy. The full walk-through is the recipe
Duolingo-style social app.
The hosted widget's friends and team surfaces show all of this without UI work
(Hosted widget).
Leagues
Weekly or daily leagues: tiers, cohorts, promotion and demotion, rank rewards in currency or items, standings, settlement and the league events.
Notifications
From event to message: message types, templates and locales, consent, quiet hours and caps, built-in sending, webhooks and connectors (Klaviyo, Braze, OneSignal, FCM), the delivery log and unsubscribe links.