AFC LogoAfrican Free Fire Community
Login

Partner Data API

How to authenticate, what each endpoint returns, and what to do when a call fails.
Connect

Base URL

https://api.africanfreefirecommunity.com/api/v1/partner/

Auth header

X-API-Key: afcp_3f9a_...

Your first call. Replace the key with the one AFC issued you, and run it from your server, never from a browser.

curl -H "X-API-Key: afcp_3f9a_YOUR_SECRET_HERE" \
  "https://api.africanfreefirecommunity.com/api/v1/partner/events/"
bash
What you get

A read-only REST API over the tournament data AFC has published to partners, for the events your organisation has been granted. An event is published to partners automatically when it finishes, and an AFC admin can also publish or withdraw one by hand, so what you read is settled data rather than a match in progress. There is no write, no delete, and no live match feed. Everything is versioned under /api/v1/, so a future breaking change ships as /api/v2/ and leaves your integration alone.

ResourceWhat it is
eventsThe event card: name, slug, dates, tier, status, prize pool.
stagesThe structure of an event, with each stage's groups nested.
matchesPer-map rows: the stage and group, match number, map, MVP, the point system, whether the result is in.
standingsAFC's official final placement for an event, with every team's points.
resultsThe whole event in one response, laid out like the tournament page: every stage, group, bracket and map, each table with its points.
teamsEvery registered team, with its country, event-wide totals and rosters.
playersEveryone who recorded stats, with their per-event stats.
designsBranded leaderboard templates: background art, placed logos, brand colours.

What the API never returns, on any plan: real names, emails, Discord IDs or any other personal data; room IDs and room passwords; AFC's internal database IDs; events AFC has not published to partners; site-wide rankings and tier ladders.

Events are always addressed by their slug, for example dynasty-cup-nigeria, never by a numeric ID. Store the slug: it is the only stable handle you are given.

Authenticating

Every request carries your key in the X-API-Key header. A key has three parts joined by underscores: the namespace afcp, a short public prefix you can safely quote in a support message, and the secret, which must never leave your servers.

A complete authenticated request.

curl -H "X-API-Key: afcp_3f9a_YOUR_SECRET_HERE" \
  "https://api.africanfreefirecommunity.com/api/v1/partner/events/"
bash

Your key is shown to the AFC admin once, at the moment it is created, and is stored only as a hash. Nobody at AFC can retrieve it afterwards. If you lose it, ask for a new one. If you think it has leaked, tell AFC immediately and they will revoke it.

A key can be revoked, deleted, or given an expiry date at issue time. An expiry date means the key works through the end of that day, UTC, which is what makes a key for a single tournament weekend practical. A revoked, deleted or expired key stops working instantly, as does every key belonging to a partner account AFC has suspended. All of those return 401. Send the key over HTTPS only, and never put it in client-side code, a mobile app, or a public repository.

If you present the correct key, the 401 body names the cause, for example Key expired. or Partner suspended., so you can tell an expiry apart from a suspension without asking. If the key itself is wrong, every failure reads Unknown or revoked key. instead. That is deliberate: a stranger guessing at keys must not be able to learn which ones exist.

Why a field or endpoint is missing

Your access has three layers, all set by AFC and all closed by default. If something you expected is not in the response, one of these three is the reason, and none of them is a bug.

First, the event has to be published to partners. Events are published automatically when they finish, and an AFC admin can publish or withdraw one by hand. Until an event is published, no partner can read it however broadly scoped. An unpublished event is a 404.

Second, resource toggles decide which endpoints answer at all: events, stages, matches, standings, teams, players, designs. If one is off for you, that endpoint returns 403 resource_not_enabled and the rest keep working.

Third, field toggles decide which fields appear inside a resource you can already read.

FieldAppears when AFC enables
placementPlacements. The best finish on teams, standings and map results, plus placement_points and booyahs.
killsKills, on teams, players, standings and map results, plus kill_points.
damageDamage, on teams, players and standings.
assistsAssists, on teams, players and standings.
rosterRosters. The player list on each team.
maps, mapMaps played. maps on a group, map on a match.
prize_poolPrize pool, on events.
mvpMVP, on matches. The in-game handle, or null if none was recorded.
banner_url, logo_url, esports_image_url, ...Images and files. Event banners and rules files, team logos, player esport images, and design artwork. Team logos and player esport images also appear on every standings row and bracket side.
rules_text, descriptionDescriptions and rules text. The event rules blurb and a team's self-description.

Always sent, whatever is switched on: each team's and player's country and country_code, every table's points with bonus_points, penalty_points, adjusted and carry_over_points, and the point system. AFC shows all of these to anyone on its public pages. other_points needs both Placements and Kills, because on its own it would give them away.

A field you are not entitled to is absent from the JSON, not present and null. Read defensively: check that a key exists rather than assuming it does. A key that is present and null means the field is enabled for you and the underlying value is genuinely empty, for example a team that never uploaded a logo.

Reading the numbers

Countries, points and the point system read the same way in every response. This is how.

Every team and player carries two fields.

{ "country": "Nigeria", "country_code": "NG" }
json

country_code is the ISO 3166-1 alpha-2 code: store it, sort by it, draw the flag from it. country is one English name per code. A team's country is the one AFC shows beside its name, worked out from where its players are; a player's is where they play from, falling back to the country on their profile. Both are null when AFC does not know, which is never an error.

Every table row, and every team's result on every map, carries points: the score it is ranked by. Its parts always add up to it exactly.

points = placement_points + kill_points + other_points
       + bonus_points - penalty_points + carry_over_points
FieldWhat it is
placement_pointsPoints for where the team finished on each map.
kill_pointsPoints for kills.
other_pointsPoints for assists and damage, when the point system awards them; for results AFC imported from another platform, whatever that platform's total held beyond placement and kills.
bonus_pointsPoints an AFC admin or the organiser added by hand.
penalty_pointsPoints an AFC admin or the organiser took away by hand.
carry_over_pointsA Point Rush head start, earned in an earlier stage.

An AFC admin or the event's organiser can add points to a team's result on a map, or take points away, for example as a penalty for breaking a rule. The table then no longer follows placements and kills alone, so the API says so: bonus_points and penalty_points carry the amounts, on the table row and on the map the change was made on, and adjusted is true on every row that includes such a change. points already includes it. AFC does not record a written reason for an adjustment, so none is sent.

A result can be corrected after you have read it. Your next sync returns the corrected numbers, so keep polling a recent event for a while after it ends.

Why a table may not look like a simple sum:

FieldsWhat happens
bonus_points, penalty_pointsAn admin or the organiser added or took away points, as above.
point_rush, carry_over_pointsA stage hands out bonus points by finishing place, carried into a later stage.
champion_point, championThe first team to win a map while already at the threshold is champion, and is listed first even with fewer points.
booyahs, killsTeams level on points are separated by booyahs, then kills, then the place in the last map played, unless the organiser set a different order.
decided_inThe final standings rank by the stage that decided each place, not by a sum of every map. See standings below.

How an event's maps are scored. placement_points is keyed by finishing place; a place not listed scores 0.

{
  "placement_points": { "1": 12, "2": 9, "3": 8, "4": 7, "5": 6, "6": 5, "7": 4, "8": 3, "9": 2, "10": 1 },
  "points_per_kill": 1,
  "points_per_assist": 0,
  "points_per_1000_damage": 0
}
json

Each map carries its own point system, because an organiser can change one map; the event carries it too when every map agrees. When maps differ, the event's point_system is null and point_system_varies is true, and you read each match's own. A stage also says whether it uses Champion Point (champion_point, with its threshold) or Point Rush (point_rush, with its reward table and the target_stage the bonus is carried into); each is null when the stage does not.

Every event carries final_stage, the stage that decides it, and final_stage_has_results. When that is false the final has not been entered on AFC: the final standings then rank the last stage that does have results, and the team at the top is that stage's leader, not the champion. Some events, scrims especially, have no results on AFC at all.

Endpoints

Nine endpoints, all GET. Any other verb returns 405. Paths below are relative to the base URL, and the slug placeholder in each path is an event slug returned by the events list. Every list endpoint is paginated; the single event and the results document are not.

EndpointToggleReturns
GET events/eventsThe events you may read, newest first.
GET events/{slug}/eventsOne event. 404 if it is not yours to read.
GET events/{slug}/stages/stagesStages in running order, each with its groups nested.
GET events/{slug}/matches/matchesThe event's maps in running order, each with its stage, group and point system. Room credentials are never included.
GET events/{slug}/standings/standingsAFC's official final placement, with points.
GET events/{slug}/results/standingsThe whole event in one document, in running order.
GET events/{slug}/teams/teamsEvery registered team with its event-wide totals.
GET events/{slug}/players/playersEveryone who recorded stats, scoped to this event.
GET events/{slug}/designs/designsBranded leaderboard templates for the event's owner.

GET events/

Returns the standard envelope. is_native_afc tells you whether AFC ran the event itself or an organiser did, without revealing which organiser. point_system is how its maps are scored, and final_stage_has_results says whether the deciding stage's results are in.

{
  "results": [
    {
      "slug": "dynasty-cup-nigeria",
      "name": "DYNASTY CUP NIGERIA",
      "competition_type": "tournament",
      "participant_type": "squad",
      "tier": "tier_3",
      "status": "completed",
      "start_date": "2026-06-29",
      "end_date": "2026-07-31",
      "is_native_afc": true,
      "point_system": { "placement_points": { "1": 12, "2": 9, "3": 8 }, "points_per_kill": 1,
                        "points_per_assist": 0, "points_per_1000_damage": 0 },
      "point_system_varies": false,
      "final_stage": "QUALIFIER FINALS",
      "final_stage_has_results": true,
      "banner_url": "https://api.africanfreefirecommunity.com/media/event_banner/DYNASTY_CUP_POSTER.png",
      "rules_file_url": "https://api.africanfreefirecommunity.com/media/event_rules/AFC_RULESET.pdf",
      "rules_text": null
    }
  ],
  "has_more": false,
  "next_offset": null,
  "total_count": 1
}
json

GET events/<slug>/stages/

Nests each stage's groups. order is a 1-based sequence number within the event, not a database ID. Stages and their groups come in the order the tournament page shows them, and each stage carries champion_point and point_rush.

{
  "stage_name": "Grand Final",
  "order": 3,
  "format": "br - normal",
  "status": "completed",
  "start_date": "2026-06-28",
  "end_date": "2026-06-28",
  "champion_point": null,
  "point_rush": null,
  "groups": [
    { "group_name": "Group A", "playing_date": "2026-06-28", "maps": ["bermuda"] }
  ]
}
json

GET events/<slug>/stages/

A stage with Point Rush, from DYNASTY CUP GRAND FINALS SSA: its finishing places earn the reward points, carried into the target stage.

{
  "stage_name": "RUSH POINT",
  "order": 2,
  "point_rush": {
    "reward": { "1": 10, "2": 7, "3": 6, "4": 3, "5": 2, "6": 1 },
    "target_stage": "GRAND FINALS"
  }
}
json

GET events/<slug>/matches/

One row per Battle Royale map, in running order, saying where it was played and how it was scored. Clash Squad bracket matches are not maps; they are in results.

{
  "stage_name": "SEMI FINALS",
  "stage_order": 1,
  "group_name": "Day 1: A+B",
  "match_number": 1,
  "result_inputted": true,
  "point_system": { "placement_points": { "1": 12, "2": 9, "3": 8 }, "points_per_kill": 1,
                    "points_per_assist": 0, "points_per_1000_damage": 0 },
  "map": "Bermuda",
  "mvp": null
}
json

GET events/<slug>/standings/

AFC's official final placement, rank ascending: the placement shown on a team's page and used for prizes.

{
  "results": [
    {
      "rank": 1,
      "team": "NEM JOGOU",
      "country": "Mozambique",
      "country_code": "MZ",
      "logo_url": "https://api.africanfreefirecommunity.com/media/teams_logos/LOGO_DA_NJ_BB_1.jpg",
      "decided_in": "GRAND FINALS",
      "reached_final_stage": true,
      "points": 118,
      "bonus_points": 0,
      "penalty_points": 0,
      "adjusted": false,
      "carry_over_points": 0,
      "placement_points": 34,
      "kill_points": 84,
      "other_points": 0,
      "matches_played": 5,
      "placement": 1,
      "booyahs": 2,
      "kills": 84
    }
  ],
  "has_more": true,
  "next_offset": 1,
  "total_count": 17
}
json

A team is placed by the last stage it played: every team that reached the grand final ranks above every team knocked out earlier, and inside a stage teams follow that stage's own table. decided_in names that stage and every number on the row is from it, so a team knocked out in the group stage shows its group-stage points. In a single-stage event this is simply that stage's table. placement is the team's best finish on a map in that stage. Solo events rank players, each row carrying username and in_game_id instead of team, and a solo table covers every map of the event.

GET events/<slug>/teams/

Every registered team sorted by name, with its event-wide totals.

{
  "team": "ALLSTARS NG",
  "team_tag": "ASN",
  "country": "Nigeria",
  "country_code": "NG",
  "status": "played",
  "logo_url": "https://api.africanfreefirecommunity.com/media/teams_logos/asn.jpg",
  "description": "We grind every night.",
  "placement": 1,
  "kills": 44,
  "roster": [
    { "username": "ASN GABBY", "in_game_id": "3098864559", "country": "Nigeria",
      "country_code": "NG", "kills": 3 }
  ]
}
json

This list is the full registration list, not the list of competitors. Registering for an AFC event and playing in one are different things, and a team that registered but never played returns zeroed stats and an empty roster, which is easy to mistake for a team that played badly. Read status to tell them apart before you draw a bracket, a standings card, or a team count. status is always present and does not depend on any toggle.

ValueMeaning
playedTurned up and played at least one match. These are the competitors.
registeredAccepted into the event, has not played a match.
waitlistedSigned up but holding a waitlist slot, not a playing slot.
pendingRegistration submitted and awaiting approval. Sponsored events only.
no_showWas expected to play and the organizer marked it absent.
withdrawnPulled out of the event.
leftLeft the event.
disqualifiedRemoved by the organizer for a rules breach.

Only one value is returned per team. Where more than one could apply, the more specific one wins, in this order: disqualified, withdrawn, left, pending, waitlisted, no_show, played, registered. So a team that played two maps and then withdrew comes back as withdrawn, not played, and its stats are a partial record of an event it did not finish. Note also that a team can hold match rows without being played: AFC seeds teams into a map before it is contested, and a team that does not turn up keeps that row with no result.

GET events/<slug>/players/

Scopes every stat to this event rather than to career totals. esports_image_url is the player's posed roster photo, for lower-thirds and versus cards, and is null for players who have not uploaded one.

{
  "username": "Ak REBORN",
  "in_game_id": "7171703030",
  "country": "Nigeria",
  "country_code": "NG",
  "esports_image_url": "https://api.africanfreefirecommunity.com/media/esports_pictures/reborn.jpg",
  "kills": 2
}
json

GET events/<slug>/results/

The whole event in one document, laid out like the tournament page, trimmed here to one row of each list. Use it to show an event properly, especially one that mixes structures.

{
  "event": {
    "slug": "dynasty-cup-grand-finals-ssa",
    "name": "DYNASTY CUP GRAND FINALS SSA",
    "point_system": null,
    "point_system_varies": true,
    "final_stage": "GRAND FINALS",
    "final_stage_has_results": true
  },
  "final_standings": [
    { "rank": 1, "team": "NEM JOGOU", "country_code": "MZ", "decided_in": "GRAND FINALS",
      "reached_final_stage": true, "points": 118 }
  ],
  "stages": [
    {
      "order": 1,
      "stage_name": "SEMI FINALS",
      "game": "battle_royale",
      "structure": "round_robin",
      "is_final_stage": false,
      "teams_qualifying": 12,
      "champion_point": null,
      "point_rush": null,
      "standings": [
        { "rank": 1, "team": "PARADOX GAMING", "country_code": "ZA", "points": 307 }
      ],
      "round_robin_groups": [
        { "label": "A", "teams": ["Alpha Wolves", "FORSE ESP"] }
      ],
      "groups": [
        {
          "group_name": "Day 1: A+B",
          "type": "lobby",
          "game_day": 1,
          "standings": [
            { "rank": 1, "team": "UNDERGROUND", "country_code": "CV", "points": 119 }
          ],
          "matches": [
            {
              "match_number": 1,
              "map": "Bermuda",
              "results": [
                { "team": "UNDERGROUND", "country_code": "CV", "played": true, "points": 28,
                  "bonus_points": 0, "penalty_points": 0, "adjusted": false,
                  "placement_points": 12, "kill_points": 16, "placement": 1, "kills": 16 }
              ]
            }
          ]
        }
      ]
    }
  ]
}
json

Every level says what it is. Each stage gives its game, battle_royale or clash_squad, and its structure: lobbies for Battle Royale groups scored on placement and kills, round_robin when teams in base groups meet across game-day lobbies, or brackets for Clash Squad. Each stage carries its own standings, the table that decides who goes through, and its groups. A group of type lobby has its table and then every map, with its point system and every team's result on it. A group of type bracket has its table and its matches. It needs the Standings toggle and is not paginated.

In a lobby, map results list the best finish first, and a team that sat a map out has played false. A team drawn into a group that has not played yet is listed at the bottom with 0 points and matches_played 0, so an upcoming group still shows who is in it. When the stage uses Champion Point, champion names the team the rule crowned, or null until it fires.

A Clash Squad bracket is played head to head, and its scores are rounds won in the set, never kills:

FieldMeaning
bracket_formatknockout, double_elimination, league or round_robin.
standingsrank, the team, wins, draws, losses, rounds_won, rounds_lost. A league or round robin adds points, 3 a win and 1 a draw; a knockout ranks by how far a team went, so it has none. rank is null for a team still alive in an unfinished bracket.
bracketwinners, losers in double elimination, third for the bronze match, or league.
team_a, team_bEach side's team, country and country_code. null is a slot waiting on an earlier match, or a bye.
score_a, score_b, winnerRounds won by each side in the set, for example 4 and 2, never kills, and the winner's name.
resultplayed, forfeit, walkover or disqualification.

With Images and files on, every table row and bracket side also carries logo_url, and solo rows esports_image_url, so you can draw a leaderboard from this one response.

GET events/<slug>/designs/

The branded templates behind this event's graphics, so you can produce on-brand standings cards yourself. The Designs toggle unlocks the endpoint; the artwork additionally needs Images and files.

{
  "name": "DYNASTY CUP",
  "design_type": "leaderboard",
  "text_color": "#FFFFFF",
  "accent_color": "#34d27b",
  "transparent_background": false,
  "max_rows": 18,
  "is_default": true,
  "background_instagram_url": "https://api.africanfreefirecommunity.com/media/org_leaderboard_designs/DYNASTY_IG.png",
  "background_youtube_url": "https://api.africanfreefirecommunity.com/media/org_leaderboard_designs/DYNASTY_YT.png",
  "logos": [
    { "image_url": "https://api.africanfreefirecommunity.com/media/org_leaderboard_logos/sponsor.png",
      "x_pct": 12.5, "y_pct": 8.0, "size": "medium" }
  ]
}
json

The two canvases are Instagram portrait at 1080x1350 and YouTube landscape at 1920x1080. Logo positions are a percentage of the canvas anchored at the logo's centre, so the same pair of numbers places a logo correctly on both sizes. max_rows is how many standings rows the template is designed to hold. You only ever receive designs belonging to the event's owner: the organiser's library for an organiser-run event, or AFC's own library for a native AFC event.

Rate limits

Requests are limited per key, in a fixed one-minute wall-clock window. Your ceiling is 60 requests per minute unless AFC agreed a different figure for your key.

Every successful response tells you where you stand, so you can slow down before you are blocked.

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
http

Exceed the limit and the next request is refused until the window rolls over.

HTTP/1.1 429 Too Many Requests
Retry-After: 60

{ "error": "rate_limit_exceeded" }
http

Watch X-RateLimit-Remaining and pause as it approaches zero, or honour Retry-After after a 429. Because the window is a wall-clock minute, a fresh allowance begins when the minute rolls over rather than sixty seconds after your first call.

Errors

Errors raised by the API carry a single error field alongside the matching HTTP status. Switch on the status code rather than on the body: a wrong HTTP verb is refused by the web framework before the API sees it and carries a detail field instead, and a request to a path that does not exist at all is not JSON.

{ "error": "not_found" }
json
StatusBodyWhat it means and what to do
401Unknown or revoked key.Your key is missing, malformed, unknown, revoked or expired, or your partner account is suspended. Check the header first, then contact AFC.
403resource_not_enabledYou are authenticated, but this resource is switched off for your account. Ask AFC to enable it.
404not_foundYou cannot read that event. Confirm the slug with AFC.
405detail, not errorEvery endpoint is GET only. This one is refused before the API sees it, so its body carries a detail field rather than error.
429rate_limit_exceededYou are over your per-minute budget. Honour the Retry-After header.

The difference between 403 and 404 is deliberate. A 403 says the resource type is switched off for you. A 404 says you cannot see that event, and it is returned whether the event is out of your scope, not yet published, or simply does not exist. The API will not confirm that an event exists if you are not allowed to read it, so a 404 is never proof that a slug is wrong.

Images, logos and artwork

Every image and file URL the API returns is absolute and publicly fetchable, so you can download it or hot-link it directly. They are plain URLs with no signature and no expiry, which means they keep working, and also means anyone you pass one to can fetch it. Treat a media URL as public, because it is.

A media field, as returned.

"logo_url": "https://api.africanfreefirecommunity.com/media/teams_logos/asn.jpg"
json

A missing asset is null, never an error. A team with no uploaded logo returns logo_url as null. The field is only absent entirely when the Images and files toggle is off for your account.

Download media once and serve it from your own storage rather than hot-linking it on every page view. Banners, logos and design artwork change rarely, your pages will render faster, and you will not be affected if AFC moves an asset. Media responses carry ETag and Last-Modified, so if you would rather revalidate than re-download, a conditional request with If-None-Match gets you a cheap 304.

Working with the API

Poll, do not hammer. Data only changes when AFC publishes or corrects a result. Once a day during a season, or once an hour around a final, is plenty. There are no webhooks.

Handle absent fields. A field you are not entitled to is missing from the JSON, so read defensively rather than assuming a key exists.

Expect a 404 to be lasting. If an event you were reading starts returning 404, AFC has either unpublished it or changed your scope. Retrying will not help, so ask AFC.

Slugs are the identifiers. Store the event slug, not the position of an event in a list, because a new event changes every position.

No key yet? Apply at africanfreefirecommunity.com/partners/apply and choose The AFC Data API. Contact your AFC partner manager for a new or rotated key, to change which events or resources you can read, or to report anything the API returns that looks wrong. Include the endpoint, the timestamp, and your key's prefix, the afcp_3f9a part, never the full key.