# Gametize Project JSON Guide (for AI project creation)

This guide explains how to build a **Gametize project JSON** that the
`POST /api/admin/v1/projects/import.json` endpoint accepts. The endpoint creates a
**new Project** from this JSON, using the same FileHub project format as the CMS
Import/Export tool.

- Machine-readable API contract: `project-tools.yaml` (served next to this guide).
- Complete worked example: `project-json-example.json`.

## 1. Top-level shape

```json
{
  "version": "chatgpt",
  "timestamp": "2026-10-24 12:00:00",
  "project": { "projectTitle": "...", "topics": [ ... ] }
}
```

| Key | Required | Notes |
| --- | --- | --- |
| `version` | No | Free-text label for the producer. Any string; ignored on import. |
| `timestamp` | No | Free-text label. Ignored on import. |
| `project` | **Yes** | The project object. Topic-only containers (root `topic` object) are **rejected** by this endpoint - it only creates new projects. |

Use **only** the keys documented here. The JSON is parsed strictly: an unknown or
misspelled key makes the whole request fail with `400 invalid_request`
("Invalid project JSON") before anything is created. Omitted optional keys simply
take platform defaults. A project with only `project.projectTitle` is valid - you
can add content later in the CMS.

## 2. `project` object

| Key | Type | Notes |
| --- | --- | --- |
| `projectTitle` | string | **Recommended.** Shown as the project name. |
| `projectDescription` | string | Short description. |
| `projectBannerImage` | URL string | Image URL (see *Images* below). |
| `projectBackgroundImage`, `projectBackgroundImage2` | URL string | Background images. |
| `heading` | HTML string | Optional rich-text heading for the project home (e.g. `"<p>Welcome</p>"`). |
| `imageCollectionTheme` | string | Optional theme name; the platform auto-assigns missing images from that theme's image collection when configured. Omit unless told to use one. |
| `projectPrivate` | boolean | `true` = invite-only project. |
| `projectDiscussionEnabled` | boolean | Turns the Community (discussion) on. |
| `challengeSearchEnabled` | boolean | Allows players to search content. |
| `guestEnabled` | boolean | Guest access - only applies if the platform allows guest mode. |
| `communityTitle`, `communityDescription`, `communityBannerImage`, `communityBackgroundImage` | string / URL | Community page appearance. |
| `storeRedemptionLimit` | integer | Redemption limit for the reward store. |
| `storePointsValidFrom`, `storePointsValidUntil` | `yyyy-MM-dd HH:mm:ss` | Points validity window. |
| `topics` | array of Topic | Main content - see below. |
| `rewards`, `achievements`, `maps`, `categories`, `rewardCategories`, `emailTemplates` | arrays | Optional project-level content - see section 6. |

## 3. Topic objects (`project.topics[]`)

| Key | Type | Notes |
| --- | --- | --- |
| `topicTitle` | string | **Recommended.** |
| `topicDescription` | string | |
| `topicBannerImage`, `topicBackgroundImage`, `topicBackgroundImage2` | URL string | Images. |
| `heading` | HTML string | Optional rich-text heading. |
| `topicType` | string | `"standard"` (default), `"personality-quiz"`, `"challenge-pool"`, `"timed"`. Use `"standard"` unless you know you need another. |
| `published` | boolean | Desired publish state. On import a topic is auto-published only if it is marked published **and** at least one of its challenges is published. |
| `topicPrivate` | boolean | Invite-only topic. |
| `allowReset` | boolean | Players can reset topic progress. |
| `completionFeedbackEnabled` | boolean | Completion feedback popup. |
| `challengeLimit` | integer | Max content items per player (used by challenge-pool topics). |
| `bonusPoints` | integer | Bonus points for completing the topic. |
| `unlockable` | boolean | Lock the topic until another topic completes. |
| `rules` | array | Advanced: unlock rules, using platform rule IDs copied from an exported file (e.g. `[{ "ruleId": <id>, "ruleCondition": "0", "triggerTopicId": 1 }]` - unlocks when the topic with reference `triggerTopicId` is completed). AI-generated projects should normally omit rules. |
| `challenges` | array of Challenge | See section 4. |
| `challengeCategories` | array | Topic-scoped content categories (`{ "categoryId": 9001, "categoryName": "..." }`). |
| `categoryIds` | array of integers | References into `project.categories` (topic categories this topic belongs to). |
| `personalityQuizTags` | array | Only for `"personality-quiz"` topics: `{ "personalityQuizTagId": 7001, "tagName": "...", "tagDescription": "..." }`. |

`topicId` may be included as a **reference ID** (any unique positive integer within
the file). It is only used to wire unlock rules between items in the same file; the
platform assigns real IDs on import.

## 4. Challenge objects (`topics[].challenges[]`)

Common keys:

| Key | Type | Notes |
| --- | --- | --- |
| `challengeTitle` | string | **Required** (item is skipped without it). |
| `challengeDescription` | string | |
| `challengeType` | string | See the table below. |
| `published` | boolean | Publish state (challenges publish by default on create). |
| `challengeImage` | URL string | Main image. |
| `challengeImageDisplayType` | string | `"shown"` or `"hidden"`. |
| `points` | integer | Points awarded on completion (e.g. `10`). |
| `points2` | integer | Secondary points (wrong-answer penalty for some types). |
| `pointsLimit`, `votePoints`, `votePointsLimit` | integer | Point limits and vote (like) points. |
| `bonusPoints`, `bonusPointsUserLimit` | integer | Bonus points settings. |
| `teamBonusPoints`, `teamBonusPointsUserRequirement`, `teamBonusPointsMode` | integer / string | Team bonus points; mode `"player"` or `"percentage"`. |
| `challengeRepeatEvery` | number | Repeat frequency in hours: `0` = only once, `0.00001`/`<1` = anytime, `1` = hourly, `12` = twice a day, `24` = daily, `168` = weekly, `720` = monthly. |
| `challengeMinViewDuration` | integer | Minimum view duration (seconds) before completion. |
| `photoOnly` | boolean | Completion requires a photo. |
| `locationOnly` | boolean | Completion requires location. |
| `completionCountLimit` | integer | Max completions per player. |
| `completionPrivate` | boolean | Completions visible to admins only. |
| `completionByAdminOnly` | boolean | Only admins can mark complete. |
| `completionMessage`, `completionMessage2` | string | Feedback shown after completion (correct/incorrect). |
| `charLimit` | integer | Completion text length limit (normal/video/form types only). |
| `unlockable` + `rules` | boolean / array | Advanced: unlock rules with platform rule IDs copied from an exported file, e.g. `[{ "ruleId": <id>, "ruleCondition": "0", "triggerChallengeId": 201 }]`. Omit in AI-generated projects. |
| `challengeMetaTime` | array | Availability windows: `{ "challengeMetaUnixStartTime": 1767225600000, "challengeMetaUnixEndTime": 1767312000000 }` (unix **milliseconds**; optional `challengeMetaTimeRepeat`). |
| `categoryIds` | array of integers | References into the topic's `challengeCategories`. |

`challengeId` may be included as a reference ID for rules, same as `topicId`.

### Supported `challengeType` values

| Value | Meaning | Extra keys |
| --- | --- | --- |
| `quiz` | Single correct answer | `options[]` **required**; exactly one `optionAnswer: true` |
| `quiz-repeat` | Repeatable quiz | `options[]` required; `repeatUntilCorrect` optional |
| `prediction` | Prediction with one intended outcome | `options[]` required; one `optionAnswer: true` |
| `survey` | Poll / opinion | `options[]` required; all `optionAnswer: false` (or use `quizType: "confirmation"` with one option) |
| `personality-quiz` | Personality quiz | Only inside a topic with `topicType: "personality-quiz"`. `options[]` required, with `tagId` references to the topic's `personalityQuizTags` |
| `multi-field` | Custom form | `fields[]` **required** |
| `normal` | Open text/photo response | no extra keys; `photoOnly` optional |
| `non-interactive` | Flashcard / info only | no points needed |
| `key` | Fixed answer key | `correctAnswerTitle` (one word) |
| `qr` | Scan-to-complete | title + points |
| `video` | Video challenge | Video embedding is **not** restored on import (the video itself does not come back) - avoid; recreate the video in the CMS |
| `external` | Minigame (reorder, spin the wheel, scratch card, memory, hangman, watch a video) | Needs `quizType` (`"reorderobjects"`, `"spinthewheel"`, `"scratchcard"`, `"memory"`, `"hangman"`, `"video"`) plus matching `options[]`; advanced - only reproduce values from an exported file |

**Not supported** (silently skipped on import): `scale`, `api`. Do not generate them.

### Quiz-type options (`options[]`)

```json
"options": [
  { "optionTitle": "Paris",  "optionAnswer": true,  "optionValue": 1, "points": 10, "completionMessage": "Correct!" },
  { "optionTitle": "Lyon",   "optionAnswer": false, "optionValue": 2 }
]
```

| Key | Notes |
| --- | --- |
| `optionTitle` | **Required.** |
| `optionAnswer` | `true` = correct / chosen answer. |
| `optionValue` | Display order number. |
| `optionImage` | Option image URL (image minigames). |
| `points` | Points for choosing this option. |
| `completionMessage` | Per-option feedback message. |
| `tagId` | Personality quiz only: reference ID of a topic `personalityQuizTags` entry. |

For quizzes, `correctAnswerTitle` may also be set (used by hangman-type answers);
`quizRandomEnabled` shuffles option order; `quizType` selects the quiz interface
(`"multiselect"` allows several correct answers).

### Multi-field (form) fields (`fields[]`)

```json
"fields": [
  { "fieldTitle": "Your name", "fieldType": "TEXT", "required": true, "points": 5 },
  { "fieldTitle": "Favourite day", "fieldType": "OPTION", "required": true,
    "options": [ { "optionTitle": "Monday", "points": 1 }, { "optionTitle": "Friday" } ] },
  { "fieldTitle": "Rate it", "fieldType": "SCALE", "required": true,
    "scale": { "scaleName": "Satisfaction", "scaleMinLabel": "Low", "scaleMaxLabel": "High",
               "scaleLevels": [ { "levelLabel": "Low", "levelNumber": 1 }, { "levelLabel": "High", "levelNumber": 5 } ] } }
]
```

| Key | Notes |
| --- | --- |
| `fieldTitle` | **Required.** |
| `fieldType` | `TEXT`, `CALENDAR` (date), `OPTION` (multiple choice), `INTEGER` (whole number), `FLOAT` (decimal), `DURATION` (time), `SCALE` (survey scale). |
| `required` | `OPTION`, `CALENDAR`, `DURATION`, `SCALE` fields are always required. |
| `points` | Per-field points. |
| `options` | For `OPTION` fields: `[{ "optionTitle": "...", "points": n }]`. |
| `scale` | For `SCALE` fields: name, min/max labels, and `scaleLevels[{ "levelLabel", "levelNumber" }]`. |
| `fieldBeginDate`, `fieldEndDate` | Allowed date range for `CALENDAR` fields (`yyyy-MM-dd HH:mm:ss`). |

## 5. Images

Image fields (`projectBannerImage`, `topicBannerImage`, `challengeImage`,
`optionImage`, `rewardImage`, `achievementImage`, `mapImage`, `categoryImage`,
community banners/backgrounds) accept **absolute `https://` URLs only**. On import
the platform copies allowed images into your own account; URLs that fail validation
(https, image extension, trusted domain) are dropped silently - the content still
imports without the image.

The recommended way to get safe image URLs is the upload endpoint:
`POST /api/admin/v1/files/upload.json` returns `fileUrl` - put that value in any
image field.

## 6. Project-level optional arrays

| Array | Item shape (main keys) | Notes |
| --- | --- | --- |
| `rewards[]` | `rewardId` (reference), `rewardTitle`, `rewardDescriptionBefore/After/AfterCollect`, `rewardImage`, `rewardSubType` (`standard`/`option`/`code`), `price`, `quantity`, `ownershipLimit`, `redemptionLimitIntervalId`, `adminToAward`, `collectionEnabled`, `collectionCancelEnabled`, `unlockable`, `rules[{ruleId, ruleValue, ruleObjectId, ruleScopeId}]`, `options[{optionTitle, quantity}]`, `categoryIds` | Rewards import **unpublished**. Voucher codes for `code` rewards do not travel in JSON. |
| `achievements[]` | `achievementId` (reference), `achievementTitle`, `achievementDescriptionBefore/After`, `achievementImage`, `rules[{ruleId, ruleValue, ruleObjectId, ruleScopeId}]` | Achievements import **unpublished**; at least one valid rule required. |
| `maps[]` | `mapId` (reference), `mapTitle`, `mapDescription`, `mapImage`, `mapType` (`default`/`desktop`/`mobile`/`tablet`), `width`, `height` (required), `hotspots[]`, `variants[]` | Maps import **unpublished**. Hotspot keys: `hotspotType` (`topic`/`topic_category`/`challenge`/`challenge_category`/`url_same_page`/`url_new_tab`), `hotspotLabel`, `hotspotMeta` (URL), `topicId`/`categoryId`/`challengeId` (file references), `xPercent`, `yPercent`, `widthPercent`, `heightPercent`. |
| `categories[]` | `categoryId` (reference), `categoryName`, `categoryDescription`, `categoryImage` | Topic categories (project-level). Same item shape is used for `rewardCategories[]` (reward store categories) and per-topic `challengeCategories[]`. |
| `emailTemplates[]` | `emailTemplateTypeId`, `emailTemplateSubject`, `emailTemplateBody` | Custom email templates for the project. |

Category reference IDs (`categoryIds`, `topic.categoryIds`, `challenge.categoryIds`,
`reward.categoryIds`) must match `categoryId` values inside the same file; they are
remapped to the newly created rows on import.

## 7. Behaviour and limits (what import actually does)

- Creates a **new** project every call. It never updates an existing project.
- Response `projectId` is the new project; `partialImportWarning` means some items
  were skipped (fix them in the CMS).
- Skipped silently: challenges without a title; `scale`/`api` challenges;
  quiz/prediction/quiz-repeat/survey/personality-quiz challenges without `options`;
  multi-field challenges without `fields`; rewards/achievements/maps without a
  required image or setting; unlock rules that reference items not in the file.
- Topics keep their relative order. Rewards, achievements, and maps are appended to
  the end of their lists.
- Scheduled publish/expiry dates are **not** supported in this format. Calendar
  availability windows (`challengeMetaTime`) are.
- The endpoint pre-parses the JSON and returns `400 invalid_request` for a
  syntactically invalid or non-project container, before anything is created.
- `projectJson` (text field) is limited per field; for large projects send the JSON
  as the `projectJsonFile` part (multipart/form-data) instead.

## 8. Minimal valid example

```json
{
  "version": "ai-assistant",
  "timestamp": "2026-10-24 12:00:00",
  "project": {
    "projectTitle": "New Hire Onboarding",
    "projectDescription": "First-week onboarding journey",
    "topics": [
      {
        "topicId": 1,
        "topicTitle": "Welcome",
        "topicType": "standard",
        "published": true,
        "challenges": [
          {
            "challengeId": 11,
            "challengeTitle": "Say hello",
            "challengeDescription": "Introduce yourself in the comments.",
            "challengeType": "normal",
            "published": true,
            "points": 10
          },
          {
            "challengeId": 12,
            "challengeTitle": "Company facts",
            "challengeType": "quiz",
            "published": true,
            "points": 10,
            "options": [
              { "optionTitle": "Founded in 2011", "optionAnswer": true, "optionValue": 1 },
              { "optionTitle": "Founded in 2001", "optionAnswer": false, "optionValue": 2 }
            ]
          }
        ]
      }
    ]
  }
}
```

See `project-json-example.json` for a richer example (quiz with feedback, survey,
key, multi-field form, non-interactive flashcard, and a reward.
