# Create leaderboard

Create one leaderboard in the community that owns your API key, with its ranking metric, schedule, audience and prizes.

- Endpoint: `POST https://api.returning.ai/v1/leaderboards`
- Section: Gamification / Leaderboards
- Authentication: `Authorization: Bearer <API_KEY>` (Community API key)
- Permission: `leaderboard` (Shown in the dashboard as "Leaderboard")
- Retries: No Idempotency-Key; list by slug before retrying
- Guide: hand-written
- Verified: code, 2026-09-27
- Last updated: 26 Sep 2026
- Web page: https://docs.returning.ai/api-reference/gamification-leaderboards/create-leaderboard

## When to use this

- Launch a monthly trading competition ranked by a user field your integration writes, such as trading volume.
- Run a time-boxed campaign that pays coins or XP to the top traders when it ends.
- Set up a leaderboard that only traders with a given role can see or be ranked on.

**Instead:** Use [Update an existing leaderboard](https://docs.returning.ai/api-reference/gamification-leaderboards/update-an-existing-leaderboard.md) instead to change a leaderboard that already exists.

**Everyone:** Send the role name `@all` in `allowedRolesToRank` and `allowedRolesToView` to include every trader. Empty rank or view lists are rejected.

**Time zone:** `timeZone` is minutes from UTC: `0` for UTC, `480` for UTC+8, `-300` for UTC-5.

## Authentication

- Header: `Authorization: Bearer <API_KEY>`
- Permission: `leaderboard`

Send a Community API key with the `leaderboard` permission. The key decides the community, so never send a community ID.

## Behaviour

The leaderboard is saved at once, after the last of your leaderboards unless you send `displayOrder`. With `isEnabled: true` it is live straight away for the traders on its view lists. Before the start date it shows as not started, and after the end date its ranks are frozen.

Scores: `XP` and `Coins` add up what each trader earns in the period, `Referrals` counts referrals completed in the period, and a user-field key adds up the numbers written to that field in the period, starting again from the last overwrite. Traders with a score of 0 are not ranked. Ranks are first calculated from the start date when the leaderboard has a reset or an end date, otherwise when someone first opens it, and then refresh about every hour.

Resets and prizes: with `enableLeaderboardReset`, scores restart every day, week, month or year at `startTime` in the leaderboard's time zone, and the finished period is kept as history. With `enablePrizePool`, each prize row's `coins` and `xp` are credited to the winners automatically when a period ends or the leaderboard reaches its end date. A leaderboard with no end date and no reset never pays prizes.

## Request

### Headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Authorization` | `string` | Yes | Community API key with `leaderboard`. (`Bearer <API_KEY>`) |
| `Content-Type` | `string` | Yes | Request body format. (`application/json`) |

### Body

Required: `name`, `slug`, `rankingField`, `displayedColumns`, `startDate`, `isEnabled`, at least one entry in a rank list (`allowedRolesToRank`, `allowedUsersToRank`, `allowedTagsToRank`) and at least one entry in a view list (`allowedRolesToView`, `allowedUsersToView`, `allowedTagsToView`). Several fields have an older alias, such as `rankBy` for `rankingField`; send one of each pair. Fields the endpoint does not recognise are ignored, so check spelling. Metrics whose names contain spaces, such as messages sent, can't be set through the API.

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | `string` | Yes | Leaderboard name shown to traders. Must be unique in your community. (1-100 chars) |
| `slug` | `string` | Yes | URL-friendly ID. Must be unique in your community. (1-100 chars; lowercase letters, numbers and single hyphens) |
| `description` | `string` | No | Description shown to traders. Defaults to empty. (Max 2000 chars) |
| `image` | `string` | No | Leaderboard image URL. (http or https URL) |
| `rankingField` | `string` | one of | What traders are ranked by: `XP` or `Coins` earned in the period, `Referrals` completed in the period, or the key of a Numerical user field, whose values written in the period are added up from the last overwrite. Other values, such as `currencies`, are read as a user-field key. (`XP`, `Coins`, `Referrals` or a user-field key; send this or `rankBy`) |
| `displayedColumns` | `array` | one of | Columns in the leaderboard table, in order. Send names, or objects to hide a column. `user` is always added as the first column. Common columns: `tier`, `level`, `xp`, `coins`, `badges`. (Up to 50; send this or `displayedFields` with `displayFieldsOrder`) |
| `displayedColumns.column` | `string` | Yes | Column name. (1-100 chars) |
| `displayedColumns.order` | `integer` | Yes | Position, starting at 0. (Min 0) |
| `displayedColumns.visible` | `boolean` | Yes | `false` keeps the column out of the table. |
| `startDate` | `string` | one of | First day of the leaderboard. It starts at `startTime` on this date, in `timeZone`. (ISO 8601 or `DD-MM-YYYY`; send this or `duration.start`) |
| `endDate` | `string` | No | Last day of the leaderboard. It ends at `endTime` on this date. Omit it for an open-ended leaderboard. (ISO 8601, `DD-MM-YYYY` or `null`) |
| `hasNoEndDate` | `boolean` | No | `true` when the leaderboard has no end date. Defaults to `false`. |
| `timeZone` | `number` | No | Time zone for the start, end and resets, as minutes from UTC. Defaults to `0` (UTC). (Minutes, e.g. `480` for UTC+8) |
| `startTime` | `object` | No | Time of day the leaderboard and each reset period start, as `hours` (1-12), `minutes` (0-59) and `ampm`. Defaults to 12:00 AM. |
| `endTime` | `object` | No | Time of day the leaderboard ends on `endDate`, in the same format. Defaults to 11:59 PM. |
| `allowedTimeFilters` | `string[]` | No | Period tabs traders can switch between: `all-time`, `daily`, `weekly`, `monthly`, `yearly`. Defaults to none. |
| `allowedRolesToRank` | `string[]` | one of | Roles whose traders get ranked, by name or ID. `@all` covers every trader in your community. (At least one rank list must have an entry) |
| `allowedUsersToRank` | `string[]` | one of | Traders to rank one by one, by platform user ID, username or email. Each must be a member of your community. (At least one rank list must have an entry) |
| `allowedTagsToRank` | `string[]` | one of | Tags whose traders get ranked, by name or ID. (At least one rank list must have an entry) |
| `allowedRolesToView` | `string[]` | one of | Roles whose traders can see the leaderboard, by name or ID. `@all` covers every trader. Traders on a rank list can always see it. (At least one view list must have an entry) |
| `allowedUsersToView` | `string[]` | one of | Traders who can see the leaderboard, by platform user ID, username or email. (At least one view list must have an entry) |
| `allowedTagsToView` | `string[]` | one of | Tags whose traders can see the leaderboard, by name or ID. (At least one view list must have an entry) |
| `isEnabled` | `boolean` | one of | `true` makes the leaderboard live at once for the traders who can see it. (Send this or `enabled`) |
| `displayOrder` | `integer` | No | Position among your leaderboards, starting at 1. Omit it or send `0` to add it last; a positive value inserts it there and moves the others down, and a value past the end adds it last. (Whole number, 0 or more) |
| `enableLeaderboardReset` | `boolean` | No | `true` restarts scores every period and keeps each finished period as history. |
| `leaderboardResetFrequency` | `string` | No | How often scores restart. Required when `enableLeaderboardReset` is `true`. (`daily`, `weekly`, `monthly`, `yearly`) |
| `enablePrizePool` | `boolean` | No | `true` pays the `prizes` rows to the winners when a period ends. |
| `prizes` | `object[]` | No | Prize rows by rank. Used only when `enablePrizePool` is `true`. |
| `prizes.isRange` | `boolean` | Yes | `true` for a range of ranks (`positionFrom` to `positionTo`), `false` for one rank (`position`). |
| `prizes.position` | `number` | No | The rank that wins, when `isRange` is `false`. |
| `prizes.positionFrom` | `number` | No | First rank in the range, when `isRange` is `true`. |
| `prizes.positionTo` | `number` | No | Last rank in the range, when `isRange` is `true`. |
| `prizes.prizeName` | `string` | Yes | Prize name shown to traders. |
| `prizes.useRewardAsPrizeName` | `boolean` | Yes | `true` shows the coin or XP amount as the prize name. |
| `prizes.coins` | `number` | Yes | Coins credited to each winning trader. Send `0` for none. |
| `prizes.xp` | `number` | Yes | XP credited to each winning trader. Send `0` for none. |
| `prizes.customFields` | `object[]` | Yes | Details the winner fills in to claim the prize. Send `[]` for none. A required field holds back the coins and XP until the trader claims. |
| `topRankOnly` | `boolean` | No | `true` blurs rows below the top `topRankLimit` ranks. Each trader still sees their own row. |
| `topRankLimit` | `number` | No | How many top ranks stay visible. Must be 1 or more when `topRankOnly` is `true`. (Whole number, 1 or more when `topRankOnly` is `true`) |
| `onlyPositiveRanks` | `boolean` | No | `true` shows only traders with a score above 0. |
| `includeDummyUsers` | `boolean` | No | Older setting for filling the table with placeholder traders. Leave it out. |
| `isPreviewEnabled` | `boolean` | No | `true` lets guests see the leaderboard, if your community allows guest preview. Defaults to `false`. |
| `widget` | `object` | No | Embeddable widget settings. Most integrations leave this out. |
| `banner` | `object` | No | Banner settings. Most integrations leave this out. |
| `translations` | `object` | No | Translated names and descriptions, as `name` and `description` lists of `languageCode` and `translation`. |
| `userInformationDisplay` | `object[]` | No | How much of each trader's details the table shows, per field, as `full` or `partial`. |
| `leaderboardPageConfig` | `object` | No | Leaderboard page layout settings. Most integrations leave this out. |
| `leaderImage` | `string` | No | Alias for `image`. `image` wins if you send both. (Up to 2,000 chars) |
| `rankBy` | `string` | No | Alias for `rankingField`. `rankingField` wins if you send both. (1-100 chars; `^[a-zA-Z0-9_-]+$`) |
| `displayedFields` | `string[]` | No | Visible columns. Alias for `displayedColumns`; send it with `displayFieldsOrder`. (Max 50 items) |
| `displayFieldsOrder` | `string[]` | No | Column order. Alias for `displayedColumns`; send it with `displayedFields`. (Max 50 items) |
| `timeFilters` | `string[]` | No | Alias for `allowedTimeFilters`. `allowedTimeFilters` wins if you send both. |
| `duration` | `object` | No | The schedule as one object. Alias for `startDate`, `endDate`, `hasNoEndDate`, `timeZone`, `startTime` and `endTime`, which win if you send them. |
| `duration.start` | `string` | Yes | First day of the leaderboard. |
| `duration.end` | `string` | No | Last day, or `null`. (Nullable) |
| `duration.noEndDate` | `boolean` | Yes | `true` when there is no end date. |
| `duration.timeZone` | `number` | Yes | Minutes from UTC. |
| `duration.startTime` | `object` | No | Start time of day. |
| `duration.startTime.hours` | `number` | No | Hour, 1-12. |
| `duration.startTime.minutes` | `number` | No | Minute, 0-59. |
| `duration.startTime.ampm` | `string` | No | `AM` or `PM`. (`AM`, `PM`) |
| `duration.endTime` | `object` | No | End time of day. |
| `duration.endTime.hours` | `number` | No | Hour, 1-12. |
| `duration.endTime.minutes` | `number` | No | Minute, 0-59. |
| `duration.endTime.ampm` | `string` | No | `AM` or `PM`. (`AM`, `PM`) |
| `rankedUserRoles` | `object` | No | The rank lists as one object. Alias for the `allowed...ToRank` fields, which win if you send them. |
| `rankedUserRoles.users` | `array` | No | Traders, by platform user ID, username or email. |
| `rankedUserRoles.roles` | `array` | No | Roles, by name or ID. |
| `rankedUserRoles.tags` | `array` | No | Tags, by name or ID. |
| `viewPermissionUserRoles` | `object` | No | The view lists as one object. Alias for the `allowed...ToView` fields, which win if you send them. |
| `viewPermissionUserRoles.users` | `array` | No | Traders, by platform user ID, username or email. |
| `viewPermissionUserRoles.roles` | `array` | No | Roles, by name or ID. |
| `viewPermissionUserRoles.tags` | `array` | No | Tags, by name or ID. |
| `performanceDisplay` | `object` | No | Table display options as one object. Alias for `topRankOnly`, `topRankLimit`, `onlyPositiveRanks` and `includeDummyUsers`, which win if you send them. |
| `performanceDisplay.showTop` | `object` | Yes | Same as `topRankOnly` (`enabled`) and `topRankLimit` (`value`). |
| `performanceDisplay.showTop.enabled` | `boolean` | Yes | Same as `topRankOnly`. |
| `performanceDisplay.showTop.value` | `number` | No | Same as `topRankLimit`. 1 or more when `enabled` is `true`. |
| `performanceDisplay.showPositive` | `object` | Yes | Same as `onlyPositiveRanks`. |
| `performanceDisplay.showPositive.enabled` | `boolean` | Yes | Same as `onlyPositiveRanks`. |
| `performanceDisplay.showDummy` | `object` | Yes | Same as `includeDummyUsers`. |
| `performanceDisplay.showDummy.enabled` | `boolean` | Yes | Same as `includeDummyUsers`. |
| `enabled` | `boolean` | No | Alias for `isEnabled`. `isEnabled` wins if you send both. |
| `previewEnabled` | `boolean` | No | Alias for `isPreviewEnabled`. `isPreviewEnabled` wins if you send both. |
| `order` | `integer` | No | Alias for `displayOrder`. `order` wins if you send both. (Min 0) |

### Example request

```bash
curl --request POST \
  --url https://api.returning.ai/v1/leaderboards \
  --header 'Authorization: Bearer <API_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
    "name": "Monthly Trading Volume",
    "slug": "monthly-trading-volume",
    "description": "Top traders by trading volume each month.",
    "rankingField": "tradingvolume",
    "displayedColumns": ["user", "tier", "level"],
    "startDate": "2026-10-01T00:00:00.000Z",
    "hasNoEndDate": true,
    "timeZone": 0,
    "allowedTimeFilters": ["monthly", "all-time"],
    "allowedRolesToRank": ["@all"],
    "allowedRolesToView": ["@all"],
    "enableLeaderboardReset": true,
    "leaderboardResetFrequency": "monthly",
    "isEnabled": true
  }'
```

## Response

A `201` returns the stored leaderboard in `data`, with defaults filled in and user, role and tag references expanded. Save `data._id`. Branch on the HTTP status.

### Response fields

| Field | Type | Presence | Description |
| --- | --- | --- | --- |
| `meta` | `object` | always | Status of the request. There is no `meta.code` on success. |
| `meta.status` | `string` | always | Result of the request. (`success`) |
| `meta.statusCode` | `integer` | always | The HTTP status, repeated. |
| `message` | `string` | always | Human-readable summary. Do not branch on it. |
| `data` | `object` | always | The new leaderboard. |
| `data._id` | `string` | - | Leaderboard ID. Save it to read, update or delete the leaderboard. |
| `data.communityID` | `string` | - | Your community ID. |
| `data.kind` | `string` | - | `custom` for a leaderboard you create. |
| `data.name` | `string` | - | The stored name. |
| `data.slug` | `string` | - | The stored slug. |
| `data.description` | `string` | - | The stored description. |
| `data.rankBy` | `string` | - | What traders are ranked by. |
| `data.displayedFields` | `string[]` | - | Visible columns. `user` comes first. |
| `data.displayFieldsOrder` | `string[]` | - | Column order. |
| `data.timeFilters` | `string[]` | - | Period tabs. |
| `data.performanceDisplay` | `object` | - | Table display options, with defaults filled in. |
| `data.duration` | `object` | - | The schedule, with defaults filled in. `timeZone` is minutes from UTC. |
| `data.rankedUserRoles` | `object` | - | Rank lists, expanded: traders as `id` (numeric platform user ID), `_id`, `username` and `email`; roles and tags as `_id` and `name`. |
| `data.viewPermissionUserRoles` | `object` | - | View lists, in the same shape. |
| `data.banner` | `object` | - | Banner settings. |
| `data.enableLeaderboardReset` | `boolean` | - | `true` when scores restart every period. |
| `data.leaderboardResetFrequency` | `string` | - | How often scores restart. |
| `data.enablePrizePool` | `boolean` | - | `true` when prizes are paid. |
| `data.prizes` | `object[]` | - | Prize rows. |
| `data.userInformationDisplay` | `object[]` | - | Trader detail display settings. |
| `data.enabled` | `boolean` | - | `true` when the leaderboard is live. |
| `data.previewEnabled` | `boolean` | - | `true` when guests can see it. |
| `data.selected` | `boolean` | - | `false`. A new leaderboard is not your community's active leaderboard. |
| `data.displayOrder` | `integer` | - | Position among your leaderboards. |
| `data.order` | `integer` | - | Same value as `displayOrder`. |
| `data.updatedAt` | `string` | - | When the leaderboard last changed, ISO 8601 UTC. (Date-time) |
| `data.createdAt` | `string` | - | When the leaderboard was created, ISO 8601 UTC. (Date-time) |
| `data.image` | `string` | - | Leaderboard image URL, when you sent one. |
| `data.widget` | `object` | - | Widget settings, when you sent them. |
| `data.translations` | `object` | - | Translations, when you sent them. |
| `data.leaderboardPageConfig` | `object` | - | Page layout settings, when you sent them. |

### Example response (201)

```json
{
  "meta": {
    "status": "success",
    "statusCode": 201
  },
  "message": "Create leaderboard success.",
  "data": {
    "_id": "66f000000000000000000301",
    "communityID": "66f000000000000000000010",
    "kind": "custom",
    "name": "Monthly Trading Volume",
    "slug": "monthly-trading-volume",
    "description": "Top traders by trading volume each month.",
    "rankBy": "tradingvolume",
    "displayedFields": ["user", "tier", "level"],
    "displayFieldsOrder": ["user", "tier", "level"],
    "timeFilters": ["monthly", "all-time"],
    "performanceDisplay": {
      "showTop": {
        "enabled": false,
        "value": 0
      },
      "showPositive": {
        "enabled": false
      },
      "showDummy": {
        "enabled": false
      }
    },
    "duration": {
      "start": "2026-10-01T00:00:00.000Z",
      "end": null,
      "noEndDate": true,
      "timeZone": 0,
      "startTime": {
        "hours": 12,
        "minutes": 0,
        "ampm": "AM"
      },
      "endTime": {
        "hours": 11,
        "minutes": 59,
        "ampm": "PM"
      }
    },
    "rankedUserRoles": {
      "users": [],
      "roles": [
        {
          "_id": "66f000000000000000000502",
          "name": "@all"
        }
      ],
      "tags": []
    },
    "viewPermissionUserRoles": {
      "users": [],
      "roles": [
        {
          "_id": "66f000000000000000000502",
          "name": "@all"
        }
      ],
      "tags": []
    },
    "banner": {},
    "enableLeaderboardReset": true,
    "leaderboardResetFrequency": "monthly",
    "enablePrizePool": false,
    "prizes": [],
    "userInformationDisplay": [],
    "enabled": true,
    "previewEnabled": false,
    "selected": false,
    "displayOrder": 2,
    "order": 2,
    "updatedAt": "2026-09-26T08:30:00.000Z",
    "createdAt": "2026-09-26T08:30:00.000Z"
  }
}
```

## Errors

`401`, `403` and `400 VALIDATION_FAILED` errors carry a code in `meta.code`. Other errors from this endpoint have no code; branch on the HTTP status and read `detail`.

### Fix the request

| Status | Code | What to do |
| --- | --- | --- |
| 400 | - | A required field is missing, a date is invalid, a user, role or tag isn't in your community, or `rankingField` names no field. There is no `meta.code`; `detail` says which, such as `startDate is required.`, `rankedUserRoles.roles[0] could not be resolved for this community.` or `Invalid rankBy value: ...`. Nothing was created. |
| 400 | `VALIDATION_FAILED` | A field breaks a format rule, such as a slug with capitals or spaces, a `rankingField` with spaces, or `topRankOnly` without a `topRankLimit` of 1 or more. `detail` names the field. Nothing was created. |
| 401 | `AUTH_API_KEY_REQUIRED` | No key was sent. Send `Authorization: Bearer <API_KEY>`. |
| 401 | `AUTH_API_KEY_INVALID` | The key is unknown, expired or malformed. Use a current Community API key. |
| 403 | `AUTH_PERMISSION_REQUIRED` | The key lacks `leaderboard`. Add the permission in Settings > Integration > API Keys. |

### Fix the data

| Status | Code | What to do |
| --- | --- | --- |
| 409 | - | Another leaderboard in your community already has this name or slug; `detail` says which (`Custom leaderboard name is already exists!` or `Leaderboard slug already exists: <slug>`). Choose another, or use the existing leaderboard. |

### Retry with backoff

| Status | Code | What to do |
| --- | --- | --- |
| 500 | - | The outcome is unknown. List leaderboards with `search=slug:<slug>` and create again only if it isn't there. |
| 502 | - | The outcome is unknown. List leaderboards with `search=slug:<slug>` and create again only if it isn't there. |
| 401 | `AUTH_API_KEY_VALIDATION_FAILED` | The key could not be checked just now, and nothing was created. Retry with backoff. |

**Retries:** Create leaderboard does not accept an `Idempotency-Key`, and a repeated name or slug returns `409`. After a timeout, a `500` or a `502`, list leaderboards with `search=slug:<slug>` and create again only if the leaderboard is missing. Over the [rate limit](https://docs.returning.ai/how-it-works.md#rate-limits), requests return `429`; wait for the window to reset, then retry.

## Next step

- [Update a user field value](https://docs.returning.ai/api-reference/user-fields-user-field-history/update-a-user-field-value.md): `POST /v1/communities/{communityId}/users/{userId}/user-fields/{fieldIdOrName}/histories`. Write each trader's numbers to the user field the leaderboard ranks by.
- [Confirm it with Get leaderboard](https://docs.returning.ai/api-reference/gamification-leaderboards/get-leaderboard.md): `GET /v1/leaderboards/{leaderboardID}`.
