One API for web, app and gateway
Everything the website does is available to the mobile app and to the WhatsApp/Instagram gateway through the same API and the same database.
Sign in with password or WhatsApp code, then use the bearer token everywhere.
Hermes Agent registers players, enters cups and delivers codes, match keys and results.
Import /api/v1/openapi.json into any client generator or API tool.
Quick start (gateway)
# 1. Register (or enrich) a player who wrote to you on WhatsApp
curl -X POST https://fntzyarena.com/api/v1/gateway/players \
-H "X-Api-Key: fa_…" -H "Content-Type: application/json" \
-d '{"phone":"+96891234567","displayName":"Saqr","epicId":"Saqr_FN","country":"OM","locale":"ar"}'
# 2. Enter them into Friday's cup (creates a team, returns a squad code)
curl -X POST https://fntzyarena.com/api/v1/gateway/entries \
-H "X-Api-Key: fa_…" -H "Content-Type: application/json" \
-d '{"phone":"+96891234567","tournament":"fantazy-friday-cup-duos","teamName":"Desert Eagles"}'
# 3. Deliver what the platform queued (codes, match keys, results), then ack
curl https://fntzyarena.com/api/v1/gateway/outbox -H "X-Api-Key: fa_…"
curl -X POST https://fntzyarena.com/api/v1/gateway/outbox/ack -H "X-Api-Key: fa_…" \
-H "Content-Type: application/json" -d '{"items":[{"id":42,"status":"sent"}]}'Machine-readable spec: /api/v1/openapi.json · Responses are { "data": … } or { "error": { "code", "message" } }.
Auth
- POST
/api/v1/auth/loginpublicSign in with email / handle / phone / Epic ID + password. Returns a bearer token.
{ "identifier": "string", "password": "string" } - POST
/api/v1/auth/otp/requestpublicSend a 6-digit sign-in code over WhatsApp (delivered by the gateway).
{ "phone": "+9689…" } - POST
/api/v1/auth/otp/verifypublicVerify the code and receive a bearer token.
{ "phone": "+9689…", "code": "123456" } - POST
/api/v1/auth/logoutbearerRevoke the current token.
Players
- GET
/api/v1/mebearerThe signed-in player: card, division, clan progress, active entries, unread count and linked accounts (Epic Games, Discord, Twitch: ids and names).
- POST
/api/v1/me/photobearerUpload a profile photo (multipart/form-data, field "file", max 4 MB). Re-encoded server-side; location/camera data removed. DELETE removes it.
- GET
/api/v1/me/invitesbearerYour invite code and link (/r/{code}), how many joined and how many counted (finished a first cup), Recruiter trophies, who invited you and your recruits.
- POST
/api/v1/me/invitesbearer"Were you invited?": add the inviter’s code or @handle. Allowed in the first week, before finishing a cup, once.
{ "code": "invite code or handle" } - GET
/api/v1/players/{handle}bearer or key · players:readPublic profile, trophies and cup history.
- GET
/api/v1/nitrobearer or key · players:readNitro: the season counting now (dates, rules), its top list and reserves; with a player token also `me` (points, rank, cups, toSeat, statement with multiplier and clan bonus) and `seats` (their Nitro cup seat: offered, confirmed, reserve …, pot and partner after the draw).
- POST
/api/v1/nitro/seatbearerAnswer your Nitro cup seat. A seat given up goes to the next reserve; errors noSeat, nitroDrawn.
{ "answer": "take | giveUp" } - GET
/api/v1/leaderboardbearer or key · players:readSeason ranking. Query: scope=all|clan, country=OM, limit, offset.
- GET
/api/v1/notificationsbearerLatest notifications, written in the player’s language (query: lang=ar|en overrides their setting). Each has priority (normal|critical), ref and actions (buttons).
- POST
/api/v1/notifications/actbearerAnswer a notification’s button: accept/decline a team invite or lineup seat, approve a join request, ready up, confirm a reported score.
{ "ref": "note.ref", "action": "accept|decline|approve|ready|confirm" } - GET
/api/v1/faqpublicThe live help page: questions and answers generated from the platform’s rules plus the Council’s own. Query: lang=ar|en.
- POST
/api/v1/notificationsbearerMark all notifications as read.
- POST
/api/v1/reportsbearerReport a fair-play problem (cheating, teaming, toxicity, smurfing, dispute, other). JSON, or multipart/form-data with up to 3 screenshots in files (private to the reporter and the Referees). Reporters hear back as a notification.
{ "kind": "cheating|teaming|toxicity|smurfing|dispute|other", "body": "string (10+ chars)", "target": "@handle or Epic name?", "tournamentId": "uuid?", "matchId": "uuid?", "links": "[url]?" } - GET
/api/v1/pollspublicPolls you can see, newest first, with tallies and your vote (sign in to see clan and tier polls). Query: open=1 for polls still taking votes. openBallot=true means admins can see who picked what.
- POST
/api/v1/polls/{id}/votebearerVote, or change your vote, while the poll is open.
{ "optionId": "o1" } - GET
/api/v1/stats/activepublicThe players who finished the most cups lately (Hermes’ weekly Top Participants). Query: days=7|30 (default 7), limit (default 10).
- GET
/api/v1/squadsbearer or key · undefinedPermanent teams: ?mine=1 (signed in) lists yours with your role and status; otherwise the public list by Glory, ?q= searches names and tags.
- POST
/api/v1/squadsbearerStart a team; you become its captain. The name may be Arabic; hue is the badge colour 0–359 (0 is red).
{ "name": "string (3–24)", "tag": "string (2–5, letters/digits)", "bio": "string?", "country": "ISO code?", "hue": "number?", "openToRequests": "boolean?" } - GET
/api/v1/squads/{slug}publicOne team: members (as each player chose to show them), cups, and for its members the join code; the captain also sees invites and requests waiting.
- POST
/api/v1/squads/{slug}/{action}bearerTeam actions, the same rules as the website: update (as POST /squads), invite { handle }, respond { accept }, request, answer { userId, approve }, remove { userId }, captain { userId }, leave, disband { confirmTag }. Returns the team afterwards (null after leave or disband).
- POST
/api/v1/squads/joinbearerAsk to join a team with its join code; the captain approves (an open team may take you straight away).
{ "code": "string" }
Tournaments
- POST
/api/v1/teams/{id}/logobearerCaptain uploads the team logo (multipart/form-data, field "file"). DELETE removes it.
- GET
/api/v1/tournaments/{slug}/standingspublicLive standings of a points cup (the cup page refreshes from here every 15 s while it's live).
- GET
/api/v1/tournamentsbearer or key · tournaments:readList cups. Query: view=upcoming|live|results|all.
- GET
/api/v1/schedulebearer or key · tournaments:readThe cup calendar: published cups plus the editions recurring series will make (projected: true, no slug yet), with titles already numbered. Query: days=30 (up to 120).
- GET
/api/v1/tournaments/{slug}bearer or key · tournaments:readCup detail: entries, standings, your team and match key when unlocked.
- GET
/api/v1/tournaments/{slug}/messagesbearerMessages from the organisers for you (everyone entered, or privately for your team: lobby codes, delays, instructions) and your conversation with them. Reading marks their replies as seen.
- POST
/api/v1/tournaments/{slug}/messagesbearerWrite to the organisers about this cup. Their answer comes back as a notification (and on WhatsApp).
{ "body": "string (≤ 1000)" } - POST
/api/v1/tournaments/{slug}/entriesbearerEnter a cup (solo) or create a team (returns a squad code).
{ "teamName": "string (team modes)" } - POST
/api/v1/tournaments/{slug}/waitlistbearerA full cup: join its waitlist (first come, first served). When a seat frees up the entry is made for you and you get a must-see note. Returns your position.
{ "teamName": "string (team cups)" } - DELETE
/api/v1/tournaments/{slug}/waitlistbearerLeave the waitlist.
- POST
/api/v1/tournaments/{slug}/waitlist/standbybearerDuring ready-up: stand by so a freed seat comes to you first (solo players are readied up automatically when they get it).
{ "on": "boolean (default true)" } - POST
/api/v1/teams/joinbearerJoin a team with a squad code.
{ "squadCode": "ABC123" } - POST
/api/v1/teams/{id}/readybearerReady up during the ready-up window.
- POST
/api/v1/teams/{id}/leavebearerLeave a team before rosters lock.
- GET
/api/v1/tournaments/{slug}/bracketbearer or key · tournaments:readKnockout / groups cups: group tables and fixtures, the knockout tree by round, the third-place match and your own match.
- POST
/api/v1/matches/{id}/reportbearerCaptains report a head-to-head series score. The other captain confirms it, or reports a different score (→ dispute for the Referees).
{ "scoreA": "number", "scoreB": "number" } - POST
/api/v1/matches/{id}/confirmbearerThe opposing captain accepts the reported score; the winner moves on.
- POST
/api/v1/matches/{id}/disputebearerThe opposing captain rejects the reported score.
{ "reason": "string?" }
Rewards
- GET
/api/v1/wallet/transfersbearerClan Arena members: recent V-Bucks transfers (both ways) and how many are left today.
- POST
/api/v1/wallet/transfersbearerClan Arena members: send spendable V-Bucks to another member by member number, at once. Errors: clanOnly, noMember, self, transferAmount, transferLimit, notEnough, transfersOff.
{ "to": "#0042", "amount": "100" } - GET
/api/v1/walletbearerYour wallet: spendable V-Bucks and cash (cash in cents with its currency), credits ready / on the way (unlocksAt) / locked (requires: epic|phone|clan|firstCup), Item Shop gift orders and cash payouts with their status, and stipends (clan tier or personal: amount, frequency daily|weekly|monthly) with their activity rules.
- POST
/api/v1/wallet/payoutsbearerClaim cash from the wallet. The amount is held until the Council pays or declines (declined = money back).
{ "amount": "number (in the platform currency)", "method": "bank|paypal|stcpay|wallet|other", "account": "string" } - GET
/api/v1/shoppublicToday’s Fortnite Item Shop: items with EN/AR names, type (outfit, pickaxe, emote, glider, wrap, bundle…), rarity, series, price in V-Bucks, image, section, bundle contents and whether it can be gifted.
- POST
/api/v1/shop/ordersbearerSend a basket from today’s shop as a gift request, paid from the wallet. The Council approves and gifts it to your Epic account, or declines (V-Bucks back).
{ "offerIds": "string[] (1–10)", "note": "string?" } - GET
/api/v1/stats/prizespublicWhat the community has won: V-Bucks, Battle Passes and cash (in cents of `currency`), including the lifetime figures from before the platform. For posters (the rewards strip) and apps.
Services
- GET
/api/v1/servicespublicThe services catalogue (coaching, scrims, replay reviews, account help…) with each service’s questions (fields: key, label/labelAr, type text|long|number|select|epic|phone|date, required, options). With a player token each service also has access: open|clanOnly|tierTooLow|full|alreadyOpen|closed.
- GET
/api/v1/services/{slug}publicOne service and its questions.
- POST
/api/v1/services/{slug}/requestsbearerAsk for a service. Answers are keyed by field key; a wrong answer returns 422 invalidAnswers with fields [{ key, error: required|invalid|tooLong }]. A price is held from the wallet (held) and returned if declined or cancelled.
{ "answers": "{ [fieldKey]: string }" } - GET
/api/v1/services/requestsbearerYour service requests, newest first: status new|in_review|approved|done|rejected|cancelled, answers, V-Bucks held and the Council’s note.
- POST
/api/v1/services/requests/{id}/cancelbearerCancel a request the Council hasn’t approved yet (V-Bucks back).
Chat
- POST
/api/chat/messages/{id}/translatebearerTranslate a chat message (to: ar | en) with Arena AI; kept once per message and language. Returns the text, or an id to poll at /api/v1/ai/tasks/{id}.
{ "to": "ar | en" } - GET
/api/chat/channelsbearerRooms you can open, with unread counts.
- GET
/api/chat/{slug}/messagesbearerMessages. Query: before, after, limit.
- POST
/api/chat/{slug}/messagesbearerSend a message.
{ "body": "string", "replyTo": "number?" } - GET
/api/chat/{slug}/streambearerLive updates (Server-Sent Events: messages, sync).
- POST
/api/chat/{slug}/readbearerMark a room as read.
- POST
/api/chat/messages/{id}/reactionsbearerToggle a reaction.
{ "emoji": "🔥 | 👑 | 😂 | 💀 | 🎯 | 👏 | GG" } - DELETE
/api/chat/messages/{id}bearerDelete your message (Referees: any message).
Arena AI
- POST
/api/v1/assistantpublic"Ask Arena": a question for Arena AI, answered from the platform's rules, cups and (signed in) the player's own account. Returns an id; poll GET /api/v1/assistant/{id} (answers usually take 5–10 s). Signed-in players get 40 questions a day, visitors 8.
{ "question": "string (3–500)", "locale": "en | ar" } - GET
/api/v1/assistant/{id}publicThe answer: status pending/running/done/failed, then answer and up to 3 links inside the platform. Only the person who asked can read it.
- POST
/api/v1/ai/translatebearerCouncil only: Arena AI drafts the other language (Modern Standard Arabic or English) of a text. Poll GET /api/v1/ai/tasks/{id}.
{ "text": "string", "to": "ar | en" } - GET
/api/v1/ai/tasks/{id}bearerA translation or answer you asked for: status and output.
- GET
/api/v1/me/coachbearerThe player's latest coach card from Arena AI (headline and three tips in both languages, from their recent cups).
- POST
/api/v1/me/coachbearer"Coach me": a new card from the player's recent cups (poll GET /api/v1/ai/tasks/{id}); at most three a day.
Arena Guard
- GET
/api/v1/guard/mebearerYour Arena Guard record: strikes in the last 30 days, a chat mute if any, and the decisions of the last 90 days with their reasons in both languages.
- POST
/api/v1/guard/cases/{id}/appealbearerAppeal a guard decision; a Referee looks again.
{ "text": "string (5–600)" } - POST
/api/chat/messages/{id}/reportbearerReport someone else's chat message: Arena AI looks at once, and a Referee sees the outcome. Posting a message can answer 423 "muted" or 400 "guardBlocked_<category>" (header X-Guard-Until when muted).
{ "reason": "string?" }
Gateway
- GET
/api/v1/gateway/ai/taskskey · ai:workThe AI executor (the worker next to Hermes) takes the next Arena AI tasks, most urgent first: { id, kind, tier fast|writer, messages, maxTokens }. wait=0–25 holds the request until work arrives.
- POST
/api/v1/gateway/ai/tasks/{id}key · ai:workThe executor posts the model's answer for a task it took (or why it couldn't); the platform checks it and applies it.
{ "content": "string?", "error": "string?", "model": "string?" } - POST
/api/v1/gateway/playerskey · players:writeCreate or enrich a player from WhatsApp / Instagram (never overwrites website edits). Invites: referralCode (the code or @handle they were given) or referredByPhone records who invited them; ownReferralCode keeps their Hermes code. Returns their own code and link.
{ "phone": "+9689…", "instagram": "handle", "displayName": "string", "epicId": "string?", "country": "OM", "platform": "pc|playstation|xbox|switch|mobile", "locale": "ar|en", "referralCode": "string?", "referredByPhone": "+9689…?", "ownReferralCode": "string?" } - POST
/api/v1/gateway/players/importkey · players:writeBulk migrate up to 500 existing players per call. Idempotent. Keeps Hermes invite codes (ownReferralCode) and who invited whom (referralCode / referredByPhone), in any order.
{ "players": "[{ phone, displayName, epicId, ownReferralCode, referralCode, … }]" } - GET
/api/v1/gateway/playerskey · players:readLook a player up by phone, instagram, epicId or handle: status, entries, tryout, and their invite code, link and counts.
- POST
/api/v1/gateway/resultskey · results:writeOne match of a points cup from a replay: every player’s placement and kills. Players are matched by Epic account id or Epic name (old names too); team rows are built from them (best placement, summed kills) and teams the replay misses keep what the Council entered. A resent replaySha is ignored. Returns matched and unmatched names; 422 duplicatePlacement lists the clashing teams.
{ "tournament": "slug", "match": "number", "replaySha": "string?", "players": "[{ epicAccountId?, epicId?, placement, kills, damage? }]" } - POST
/api/v1/gateway/reportskey · players:writeA player reports a fair-play problem over WhatsApp / Instagram (with clip links); it lands in the Referees’ queue.
{ "phone": "+9689…", "kind": "cheating|teaming|toxicity|smurfing|other", "body": "string", "target": "@handle or Epic name?", "tournament": "slug?", "links": "[url]?" } - GET
/api/v1/gateway/publish-jobskey · studio:writeStudio jobs for Hermes (also sent as render_request / publish_request outbox messages). Query: status=requested,approved.
- POST
/api/v1/gateway/publish-jobs/{id}/assetskey · studio:writeHermes delivers the rendered files for a requested piece (https links from its media library, images or videos, up to 10) and caption drafts; the Council reviews them in the studio.
{ "assets": "[{ url, kind: image|video, label? }]", "captionEn": "string?", "captionAr": "string?" } - POST
/api/v1/gateway/newskey · news:writeHermes suggests Fortnite news from the feeds it watches (Epic news, server status, patch notes), up to 20 at a time. Each waits on the Council's News page to be edited and published, or dismissed; a repeated id is ignored.
{ "items": "[{ id, source, url?, imageUrl?, title, titleAr?, body, bodyAr?, urgent? }]" } - POST
/api/v1/gateway/publish-jobs/{id}/receiptskey · studio:writeHermes reports an approved piece as published (each target with the platform’s id and/or permalink; at least one is required) or failed.
{ "status": "published|failed", "receipts": "[{ target, id?, url?, error? }]", "error": "string?" } - POST
/api/v1/gateway/entrieskey · tournaments:writeEnter a player into a cup or join a team by squad code. With waitlist: true a full cup puts the player on its waitlist instead (202 with their position).
{ "phone": "+9689…", "tournament": "slug", "teamName": "string?", "waitlist": "boolean?", "squadCode": "string?" } - GET
/api/v1/gateway/verificationkey · players:readQuery: phone or instagram. Whether the player is verified (معتمد), the checklist (epic, phone, friendAccount) and their last request (pending / approved / rejected with the Council note).
- POST
/api/v1/gateway/verificationkey · players:writeThe approval form collected on WhatsApp / Instagram joins the Council verification queue; the decision comes back as outbox verification_update. Errors: elig_epic, friendRequest, verifyPending, alreadyVerified.
{ "phone": "+9689…", "friendRequest": "boolean", "note": "string?" } - POST
/api/v1/gateway/checkinkey · tournaments:writeReady up for a cup (the reply to a ready_up message). Only in the ready-up window and for a full team; errors: notEntered, checkinClosed, rosterIncomplete.
{ "phone": "+9689…", "tournament": "slug" } - POST
/api/v1/gateway/withdrawkey · tournaments:writeLeave a cup ("I can’t make it") before it locks, freeing the seat; errors: notEntered, locked, inBracket.
{ "phone": "+9689…", "tournament": "slug" } - POST
/api/v1/gateway/tryoutskey · clan:writeSubmit a Clan Arena tryout.
{ "phone": "+9689…", "message": "string", "answers": "{ hours, bestPlacement, role }" } - GET
/api/v1/gateway/messageskey · tournaments:readQuery: phone (or instagram) and tournament. What the organisers sent this player about the cup (lobby codes, updates) and their conversation.
- POST
/api/v1/gateway/messageskey · tournaments:writeA player’s WhatsApp / Instagram message to the organisers about a cup; it lands in the Council inbox.
{ "phone": "+9689…", "tournament": "slug", "body": "string (≤ 1000)" } - GET
/api/v1/gateway/nitrokey · players:readQuery: phone (or instagram). “My Nitro points”: the player’s points, place, statement and seat, with the season list (same shape as /api/v1/nitro).
- POST
/api/v1/gateway/nitrokey · tournaments:writeThe player answers their Nitro cup seat from WhatsApp (the reply to a nitro_seat message). Proven numbers only.
{ "phone": "+9689…", "answer": "take | giveUp" } - POST
/api/v1/gateway/transferskey · wallet:writeA Clan Arena member sends V-Bucks to another member (member number), same limits as the website. Proven numbers only; confirm the recipient’s name with the player first.
{ "phone": "+9689…", "to": "#0042", "amount": "100" } - POST
/api/v1/gateway/replayskey · tournaments:writeIntegrity partner: a clan member who played sends one link to their replay files within the window; errors clanOnly, notPlayed, replayWindow, replaySlotsFull, replayAlready.
{ "phone": "+9689…", "tournament": "slug", "url": "https://…" } - GET
/api/v1/gateway/walletkey · wallet:readQuery: phone (or instagram), proven numbers only. The player’s wallet, so the assistant can answer “how many V-Bucks do I have?” and “where is my gift?”.
- POST
/api/v1/gateway/orderskey · wallet:writeA player picks items from today’s shop on WhatsApp / Instagram; the gift request (paid from their wallet) joins the Council’s queue.
{ "phone": "+9689…", "offerIds": "string[] (1–10)", "note": "string?" } - POST
/api/v1/gateway/payoutskey · wallet:writeA player claims cash on WhatsApp (proven number only); it joins the Council’s payout queue.
{ "phone": "+9689…", "amount": "number", "method": "bank|paypal|stcpay|wallet|other", "account": "string" } - POST
/api/v1/gateway/shopkey · shop:writeFeed the day’s Item Shop (when Hermes already builds the daily shop post). GET returns today’s shop.
{ "day": "YYYY-MM-DD?", "items": "[{ offerId, name, nameAr?, type, rarity, price, image?, section?, giftable? }]" } - GET
/api/v1/gateway/serviceskey · services:writeThe services catalogue for the assistant’s menu, with each service’s questions. With phone (or instagram) each service also says whether that player can ask for it now (access).
- POST
/api/v1/gateway/services/{slug}/requestskey · services:writeA player asks for a service on WhatsApp / Instagram (create or enrich the player first). Same rules and queue as the website; 422 invalidAnswers lists the answers to ask again. Status changes come back through the outbox as service_update.
{ "phone": "+9689…", "answers": "{ [fieldKey]: string }" } - GET
/api/v1/gateway/services/requestskey · services:writeQuery: phone (or instagram). The player’s service requests and where each one is.
- GET
/api/v1/gateway/outboxkey · outbox:readMessages to deliver: otp, ready_up (ready-up is open for a full team: ask the player to reply, then POST /gateway/checkin), roster_incomplete (to a captain whose team is short when ready-up opens), verification_update (approved / rejected with the Council note), match_key, match_ready, bracket_draw, tournament_results, tournament_announced, announcement, referral_joined, referral_counted, cup_message, cup_reply, wallet_update (gift or payout status), service_update (service request status), social_post (channel "social": public posts in EN/AR with a link and image).
- POST
/api/v1/gateway/outbox/ackkey · outbox:ackAcknowledge delivery.
{ "items": "[{ id, status: sent|failed, error? }]" }
Council
- GET
/api/v1/councilkey · council:readFor an operator key (made by the Founder on Council → Gateway): the Council account it acts as (@hermes), the queues waiting (count, oldest, urgent), and every Council action with its scope, the power it needs, and whether this key may use it now.
- GET
/api/v1/council/queues/{queue}key · council:readOne page of a queue, oldest first: gifts, payouts (account masked), verify, tryouts, reports, services or guard. ?limit= up to 50. Money requests list linked accounts (same device, payout account or Epic account).
- GET
/api/v1/council/players/{handle}key · council:readA short player summary: role, tier, Glory, Epic (and whether it is proven), verified, open reports, wallet, linked accounts. No phone, email or payout details.
- POST
/api/v1/council/actionskey · council:actDoes one Council action as the key’s account, under every Council rule (powers, conflicts of interest, linked accounts, decided once, audit log). Money actions (gift.approve, gift.gifted, payout.approve, payout.paid, wallet.grant, cup.finalize) need council:money. Errors: forbidden, linkedToYou, grantedByYou, resetByYou, ownRequest, alreadyDecided, notFound.
{ "action": "gift.approve | gift.gifted | gift.reject | payout.approve | payout.paid | payout.reject | wallet.grant | verification.approve | verification.reject | application.approve | application.reject | report.resolve | report.dismiss | service.decide | guard.uphold | guard.overturn | chat.mute | cup.status | cup.finalize", "id": "uuid (most actions)", "note": "string?" }