# Create match prediction

Create a match prediction as a draft in the community that owns your API key, ready to edit and then publish.

- Endpoint: `POST https://api.returning.ai/v1/match-predictions`
- Section: Gamification / Match predictions
- Authentication: `Authorization: Bearer <API_KEY>` (Community API key)
- Permission: `matchPredictions` (Shown in the dashboard as "Match Prediction")
- Retries: Idempotency-Key required; reuse it only for the same body
- Guide: hand-written
- Verified: code, 2026-09-27
- Last updated: 26 Sep 2026
- Web page: https://docs.returning.ai/api-reference/gamification-match-predictions/create-match-prediction

## When to use this

- A fixture is announced and you want a prediction game for it.
- Your sports data feed creates one match prediction per fixture, keyed by your own fixture ID.
- You want to set up the teams, times and rewards now and publish later.

**Instead:** Use [Patch match prediction](https://docs.returning.ai/api-reference/gamification-match-predictions/patch-match-prediction.md) instead to change a match prediction that already exists.

**Availability:** api.returning.ai doesn't serve this route yet: requests return an empty `404`. Ask Returning.AI before you build on it.

## Authentication

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

Use a Community API key with `matchPredictions`, shown in the dashboard as Match Prediction, and keep it on your server. The key decides the community: you only see and change match predictions in that community, and an ID from another community returns `404`, the same as an unknown ID. Personal API keys are rejected with `403`.

## Behaviour

api.returning.ai doesn't serve this route yet: requests return an empty `404`. Ask Returning.AI before you build on it.

A new match prediction is a `draft`: traders can't see it until you publish it. Drafts also appear with your other mini-games in the dashboard, where your team can edit or delete them, and match predictions your team creates there are returned by these endpoints too.

## Request

### Headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Authorization` | `string` | Yes | Community API key with `matchPredictions`. (`Bearer <API_KEY>`) |
| `Idempotency-Key` | `string` | Yes | A unique key you create for this match prediction, such as a UUID. Reuse it only to retry the same body. (1-255 characters) |
| `Content-Type` | `string` | Yes | Request body format. Anything else returns `415`. (`application/json`) |
| `X-Request-Id` | `string` | No | Your own ID for this request, returned in `meta.requestId`. Without it, a `req_...` ID is generated. |

### Body

Required: `title`, `teamA.name`, `teamB.name`, `predictionStartAt`, `lockAt`, `kickoffAt`, both reward tiers, `entryCost` and `viewPermission`. Every other field is optional, and a field that isn't listed here is rejected at any level.

Send times as ISO 8601 in UTC ending in `Z`, such as `2026-10-10T19:00:00.000Z`; an offset such as `+08:00` is rejected. `predictionStartAt` must be at or before `lockAt`, `lockAt` at or before `kickoffAt`, and `kickoffAt` in the future.

Send your fixture ID as `externalReference`. It is unique in your community, so a repeated create returns `409 EXTERNAL_REFERENCE_CONFLICT` instead of a second match. Role and user IDs in `viewPermission` must belong to your community.

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | `string` | Yes | The match title traders see. (1-256 characters, trimmed) |
| `description` | `string` | No | Longer text for traders. (Up to 10,000 characters) |
| `externalReference` | `string` | No | Your own fixture ID. Unique in your community, so it is the safest way to find the match again. (1-128 characters, trimmed) |
| `teamA` | `object` | Yes | The first team. Replaced as a whole. |
| `teamA.name` | `string` | Yes | Team name. (1-128 characters) |
| `teamA.imageUrl` | `string` | No | Team logo URL. (A URL, up to 2,048 characters) |
| `teamB` | `object` | Yes | The second team. Replaced as a whole. |
| `teamB.name` | `string` | Yes | Team name. (1-128 characters) |
| `teamB.imageUrl` | `string` | No | Team logo URL. (A URL, up to 2,048 characters) |
| `predictionStartAt` | `string` | Yes | When traders can start predicting. (ISO 8601 in UTC ending in `Z`; at or before `lockAt`) |
| `lockAt` | `string` | Yes | When predictions close. (ISO 8601 in UTC ending in `Z`; at or before `kickoffAt`) |
| `kickoffAt` | `string` | Yes | When the match starts. You can settle only after this time. (ISO 8601 in UTC ending in `Z`; in the future) |
| `scoreBasis` | `string` | No | Which score you will settle on. Defaults to `regulation`. (`regulation` or `extra_time`) |
| `rewards` | `object` | Yes | What winners earn. Send both tiers. |
| `rewards.correctResult` | `object` | Yes | Paid for picking the right result: team A, team B or a draw. |
| `rewards.correctResult.coins` | `integer` | Yes | Coins. (Whole number, 0-1,000,000) |
| `rewards.correctResult.xp` | `integer` | Yes | XP. (Whole number, 0-1,000,000) |
| `rewards.exactScoreBonus` | `object` | Yes | Paid on top of `correctResult` when the called score is also exact. |
| `rewards.exactScoreBonus.coins` | `integer` | Yes | Coins. (Whole number, 0-1,000,000) |
| `rewards.exactScoreBonus.xp` | `integer` | Yes | XP. (Whole number, 0-1,000,000) |
| `entryCost` | `object` | Yes | What a trader pays to predict. Winners get it back at settlement, and everyone gets it back if you void. |
| `entryCost.enabled` | `boolean` | Yes | `true` to charge coins for each prediction. |
| `entryCost.coins` | `integer` | Yes | Coins per prediction. (Whole number; `0` when `enabled` is `false`) |
| `viewPermission` | `object` | Yes | Who can see the match once it is published. Can be empty on a draft; publishing needs at least one role or user. |
| `viewPermission.roleIds` | `string[]` | Yes | Role IDs from your community. Traders with any of these roles can see it. (Role IDs in your community) |
| `viewPermission.userIds` | `string[]` | Yes | Platform user IDs, as strings, of traders who can see it. (Members of your community) |

### Example request

```bash
curl --request POST \
  --url https://api.returning.ai/v1/match-predictions \
  --header 'Authorization: Bearer <API_KEY>' \
  --header 'Idempotency-Key: 3f9c2a7e-1b4d-4e8a-9c61-2d5f7a8b9e10' \
  --header 'Content-Type: application/json' \
  --data '{
    "title": "Australia vs New Zealand",
    "description": "Predict the final score.",
    "externalReference": "provider-fixture-123",
    "teamA": {
      "name": "Australia",
      "imageUrl": "https://cdn.example.com/teams/australia.png"
    },
    "teamB": {
      "name": "New Zealand",
      "imageUrl": "https://cdn.example.com/teams/new-zealand.png"
    },
    "predictionStartAt": "2026-10-01T08:00:00.000Z",
    "lockAt": "2026-10-10T18:55:00.000Z",
    "kickoffAt": "2026-10-10T19:00:00.000Z",
    "scoreBasis": "regulation",
    "rewards": {
      "correctResult": {
        "coins": 100,
        "xp": 50
      },
      "exactScoreBonus": {
        "coins": 200,
        "xp": 100
      }
    },
    "entryCost": {
      "enabled": false,
      "coins": 0
    },
    "viewPermission": {
      "roleIds": ["66f000000000000000000806"],
      "userIds": []
    }
  }'
```

## Response

A `201` returns the whole match prediction, with `status` `draft`, `isPublished` `false` and `version` `1`. Save `data.id` for every later call.

### Response fields

| Field | Type | Presence | Description |
| --- | --- | --- | --- |
| `meta` | `object` | always | Result details. |
| `meta.status` | `string` | always | Result of the request. (`success`) |
| `meta.statusCode` | `integer` | always | The HTTP status, repeated. (`201`) |
| `meta.code` | `string` | always | Machine-readable result code. (`MATCH_PREDICTION_CREATED`) |
| `meta.requestId` | `string` | always | Your `X-Request-Id` if you sent one, otherwise a generated `req_...` ID. Quote it when you contact support. |
| `message` | `string` | always | Human-readable summary. Do not branch on it. |
| `data` | `object` | always | The new match prediction. |
| `data.id` | `string` | always | The match prediction ID. Save it. (`mp_` and 24 hex characters) |
| `data.externalReference` | `string` | - | Your own fixture ID. Left out when not set. |
| `data.title` | `string` | always | The match title traders see. |
| `data.description` | `string` | - | Longer text for traders. Left out when not set. |
| `data.teamA` | `object` | always | The first team. |
| `data.teamA.name` | `string` | always | Team name. |
| `data.teamA.imageUrl` | `string` | - | Team logo URL. Left out when not set. |
| `data.teamB` | `object` | always | The second team. |
| `data.teamB.name` | `string` | always | Team name. |
| `data.teamB.imageUrl` | `string` | - | Team logo URL. Left out when not set. |
| `data.predictionStartAt` | `string` | always | When traders can start predicting, in UTC. Milliseconds always read `.000`. |
| `data.lockAt` | `string` | always | When predictions close, in UTC. |
| `data.kickoffAt` | `string` | always | When the match starts, in UTC. |
| `data.scoreBasis` | `string` | always | Which score you settle on. Settlement uses the score you send either way. (`regulation` or `extra_time`) |
| `data.viewPermission` | `object` | always | Who can see the match once it is published. |
| `data.viewPermission.roleIds` | `string[]` | always | Role IDs. Traders with any of these roles can see it. |
| `data.viewPermission.userIds` | `string[]` | always | Platform user IDs, as strings, of traders who can see it. |
| `data.rewards` | `object` | always | What winners earn. |
| `data.rewards.correctResult` | `object` | always | Paid for picking the right result: team A, team B or a draw. |
| `data.rewards.correctResult.coins` | `integer` | always | Coins. |
| `data.rewards.correctResult.xp` | `integer` | always | XP. |
| `data.rewards.exactScoreBonus` | `object` | always | Paid on top of `correctResult` when the called score is also exact. |
| `data.rewards.exactScoreBonus.coins` | `integer` | always | Coins. |
| `data.rewards.exactScoreBonus.xp` | `integer` | always | XP. |
| `data.entryCost` | `object` | always | What a trader pays to predict. |
| `data.entryCost.enabled` | `boolean` | always | `true` when predicting costs coins. |
| `data.entryCost.coins` | `integer` | always | Coins per prediction. `0` when `enabled` is `false`. |
| `data.status` | `string` | always | Always `draft` for a new match prediction. |
| `data.isPublished` | `boolean` | always | `true` once published. |
| `data.acceptingPredictions` | `boolean` | always | `true` while the match is `open` and the time is between `predictionStartAt` and `lockAt`. |
| `data.attemptCount` | `integer` | always | Predictions traders have submitted. |
| `data.settlement` | `object` | always | Always `null` here. Read the match with Get match prediction for its settlement totals. |
| `data.version` | `integer` | always | Always `1` for a new match prediction. Send it with your first Patch match prediction. |
| `data.createdAt` | `string` | always | When the match prediction was created. |
| `data.updatedAt` | `string` | always | When it last changed. |

### Example response (201)

```json
{
  "meta": {
    "status": "success",
    "statusCode": 201,
    "code": "MATCH_PREDICTION_CREATED",
    "requestId": "req_66f00000000000000000000000000811"
  },
  "message": "Match prediction created.",
  "data": {
    "id": "mp_66f000000000000000000801",
    "title": "Australia vs New Zealand",
    "teamA": {
      "name": "Australia",
      "imageUrl": "https://cdn.example.com/teams/australia.png"
    },
    "teamB": {
      "name": "New Zealand",
      "imageUrl": "https://cdn.example.com/teams/new-zealand.png"
    },
    "predictionStartAt": "2026-10-01T08:00:00.000Z",
    "lockAt": "2026-10-10T18:55:00.000Z",
    "kickoffAt": "2026-10-10T19:00:00.000Z",
    "scoreBasis": "regulation",
    "viewPermission": {
      "roleIds": ["66f000000000000000000806"],
      "userIds": []
    },
    "rewards": {
      "correctResult": {
        "coins": 100,
        "xp": 50
      },
      "exactScoreBonus": {
        "coins": 200,
        "xp": 100
      }
    },
    "entryCost": {
      "enabled": false,
      "coins": 0
    },
    "status": "draft",
    "isPublished": false,
    "acceptingPredictions": false,
    "attemptCount": 0,
    "settlement": null,
    "version": 1,
    "createdAt": "2026-09-27T09:00:00.000Z",
    "updatedAt": "2026-09-27T09:00:00.000Z",
    "externalReference": "provider-fixture-123",
    "description": "Predict the final score."
  }
}
```

## Errors

Every error carries its code in `meta.code` and the request's ID in `meta.requestId`. Validation errors list each problem in `details`, with a `field`, `code` and `message`. The API key checks (`401`, `403`, `404 COMMUNITY_NOT_FOUND` and `500 AUTHENTICATION_FAILED`) return `detail` and `solution` instead. A `404` with an empty body means the route isn't available on api.returning.ai yet.

### Fix the request

| Status | Code | What to do |
| --- | --- | --- |
| 404 | - | The body is empty: api.returning.ai doesn't serve this route yet. Ask Returning.AI before you build on it. |
| 400 | `MATCH_PREDICTION_VALIDATION_FAILED` | A field is missing, breaks its rule or isn't recognised; the times are out of order; `kickoffAt` isn't in the future; or `entryCost.coins` isn't `0` while `entryCost.enabled` is `false`. `details` lists each problem. A role or user in `viewPermission` that isn't in your community gives `INVALID_REFERENCE`, with `message` `Match prediction configuration is invalid.` Nothing was saved. |
| 400 | `READ_ONLY_FIELD` | The request has a field the server sets, such as `id`, `status`, `version` or `createdAt`. `details` names it. Remove it. |
| 400 | `INVALID_JSON` | The body is not valid JSON. Fix the syntax and send it again. |
| 400 | `IDEMPOTENCY_KEY_REQUIRED` | Send an `Idempotency-Key` header with a unique value. |
| 400 | `IDEMPOTENCY_KEY_INVALID` | The `Idempotency-Key` is longer than 255 characters. Use a shorter key, such as a UUID. |
| 415 | `UNSUPPORTED_MEDIA_TYPE` | Send the body as JSON with `Content-Type: application/json`. |
| 401 | `AUTHENTICATION_REQUIRED` | The key is missing, invalid or expired. Send `Authorization: Bearer <API_KEY>` with a current Community API key. |
| 403 | `API_KEY_PERMISSION_DENIED` | The key lacks `matchPredictions`, or it is a personal key. Use a Community API key and add Match Prediction in Settings > Integration > API Keys. |
| 409 | `IDEMPOTENCY_KEY_CONFLICT` | This `Idempotency-Key` was already used with a different body, or the first request with it is still running. Use a new key for a new body. |

### Fix the data

| Status | Code | What to do |
| --- | --- | --- |
| 404 | `COMMUNITY_NOT_FOUND` | The community this key belongs to no longer exists. Contact Returning.AI support. |
| 409 | `EXTERNAL_REFERENCE_CONFLICT` | Another match prediction in your community already has this `externalReference`. Find it with List match predictions instead of creating it again. |

### Retry with backoff

| Status | Code | What to do |
| --- | --- | --- |
| 429 | `RATE_LIMITED` | Your community is over its [rate limit](https://docs.returning.ai/how-it-works.md#rate-limits). Wait for the seconds in `Retry-After`, then retry. |
| 500 | `INTERNAL_ERROR` | The outcome is unclear. Look the match up with List match predictions and your `externalReference`, and create it again with a new key only if it isn't there. |
| 500 | `AUTHENTICATION_FAILED` | The key could not be checked. Retry with backoff; nothing changed. |
| 503 | `MATCH_PREDICTION_FROZEN` | Returning.AI has paused match prediction requests. Nothing changed. Retry later with backoff. |

**Retries:** Every create needs an `Idempotency-Key`. Sending the same key with the same body within 24 hours returns the first response again and creates nothing new, even when that first response was an error. The same key with a different body returns `409 IDEMPOTENCY_KEY_CONFLICT`. One exception: a body that differs only in `predictionStartAt`, `lockAt` or `kickoffAt` counts as the same body and gets the first response back, so use a new key whenever you change anything.

After a timeout or a `500`, look the match up with List match predictions and your `externalReference`, and create it again with a new key only if it isn't there. Returning.AI can set a request limit for your community on these endpoints, counted per community and calling IP address. Over it, requests return `429 RATE_LIMITED` with a `Retry-After` header in seconds; wait that long, then retry.

## Next step

- [Publish match prediction](https://docs.returning.ai/api-reference/gamification-match-predictions/publish-match-prediction.md): `POST /v1/match-predictions/{matchPredictionId}/publish`. Publish the draft when it is ready, so the traders in `viewPermission` can see it.
- [Read the draft back with Get match prediction](https://docs.returning.ai/api-reference/gamification-match-predictions/get-match-prediction.md): `GET /v1/match-predictions/{matchPredictionId}`.
