{
  "info": {
    "_postman_id": "a1b2c3d4-e5f6-7890-abcd-1234567890ab",
    "name": "Booking Window - Activity Out API & Management",
    "description": "Postman Collection for the Activity Out API (/xApi/*), API Users CRUD (/api_user/*), and the Activity Markup Module CRUD (/activity_markup/*).\n\nAUTH (2026-08-31): password + emailed OTP, and /xApi/* no longer accepts X-API-KEY.\n  1. \"1. Login (send OTP)\"  -> mails a 6-digit code, stores {{otp_token}} (5 min)\n  2. \"2. Verify OTP\"        -> paste the code into {{otp_code}}, stores {{xapi_token}} (24h)\n  3. \"3. Activity List\"     -> sends Authorization: Bearer {{xapi_token}}\n\nThe code is also written to activity_users.mail_otp, so during development you can read it from the table instead of waiting for the mail. 3 wrong attempts burn it and you must log in again.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "variable": [
    {
      "key": "baseUrl",
      "value": "http://localhost:5558",
      "type": "string"
    },
    {
      "key": "x_api_key",
      "value": "DMC_TEST12345",
      "type": "string"
    },
    {
      "key": "xapi_username",
      "value": "partner@globaldmc.com",
      "type": "string"
    },
    {
      "key": "xapi_password",
      "value": "ChangeMe123!",
      "type": "string"
    },
    {
      "key": "xapi_token",
      "value": "",
      "type": "string"
    },
    {
      "key": "otp_token",
      "value": "",
      "type": "string"
    },
    {
      "key": "otp_code",
      "value": "",
      "type": "string"
    }
  ],
  "item": [
    {
      "name": "1. API Users Management",
      "item": [
        {
          "name": "Register API User",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"role_id\": 3,\n  \"name\": \"Global DMC Partner\",\n  \"email\": \"partner@globaldmc.com\",\n  \"mobile\": \"9876543210\",\n  \"company_name\": \"Global DMC Travels Ltd\",\n  \"website\": \"https://globaldmc.com\",\n  \"country_name\": \"India\",\n  \"city\": \"Mumbai\",\n  \"currency_code\": \"INR\",\n  \"credit_limit\": 50000.00,\n  \"verified\": 1,\n  \"status\": 1\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/api_user/register",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api_user",
                "register"
              ]
            },
            "description": "Registers a new API user (DMC, AGENT, MEMBER). Generates unique `user_reference_id` (X-API-KEY) if not specified."
          },
          "response": []
        },
        {
          "name": "Update API User",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": 1,\n  \"company_name\": \"Global DMC Travels Private Limited\",\n  \"credit_limit\": 75000.00,\n  \"currency_code\": \"INR\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/api_user/update",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api_user",
                "update"
              ]
            },
            "description": "Updates profile details for an API User by `id` or `user_reference_id`."
          },
          "response": []
        },
        {
          "name": "List API Users",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"page\": 1,\n  \"limit\": 10,\n  \"role_id\": 3,\n  \"status\": 1,\n  \"search\": \"DMC\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/api_user/list",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api_user",
                "list"
              ]
            },
            "description": "Lists API users with filtering by role_id, status, search keyword, and pagination."
          },
          "response": []
        },
        {
          "name": "API User Details",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": 1\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/api_user/details",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api_user",
                "details"
              ]
            },
            "description": "Fetches details of a specific API user by `id` or `user_reference_id`."
          },
          "response": []
        },
        {
          "name": "Update User Status",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": 1,\n  \"status\": 1,\n  \"verified\": 1\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/api_user/status_update",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api_user",
                "status_update"
              ]
            },
            "description": "Toggles active status (1/0) or verification flag (1/0) of an API User."
          },
          "response": []
        },
        {
          "name": "Delete API User",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": 1\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/api_user/delete",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api_user",
                "delete"
              ]
            },
            "description": "Deletes an API user record by `id` or `user_reference_id`."
          },
          "response": []
        }
      ]
    },
    {
      "name": "2. Activity Markup Module",
      "item": [
        {
          "name": "Create Markup Rule",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"title\": \"Standard Out-API Partner 10% Markup\",\n  \"activities_ids\": \"101,102,105\",\n  \"cities\": \"Singapore,Dubai\",\n  \"country\": \"Singapore\",\n  \"price_from\": 0.00,\n  \"price_to\": 10000.00,\n  \"score\": 10,\n  \"currency\": \"INR\",\n  \"markup_type\": \"percentage\",\n  \"markup_amount_fix\": 0.00,\n  \"markup_amount_percentage\": 10.00,\n  \"discount_amount_fix\": 0.00,\n  \"discount_amount_percentage\": 0.00,\n  \"status\": 1,\n  \"is_default\": 0,\n  \"api_partners\": \"DMC_TEST12345\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/activity_markup/create",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "activity_markup",
                "create"
              ]
            },
            "description": "Creates a new activity markup rule targeting specific activities, cities, country, price ranges, score, and API partners."
          },
          "response": []
        },
        {
          "name": "Update Markup Rule",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": 1,\n  \"title\": \"Updated Out-API Partner 12% Markup\",\n  \"markup_amount_percentage\": 12.00,\n  \"score\": 15\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/activity_markup/update",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "activity_markup",
                "update"
              ]
            },
            "description": "Updates an existing markup rule by `id`."
          },
          "response": []
        },
        {
          "name": "List Markup Rules",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"page\": 1,\n  \"limit\": 10,\n  \"status\": 1,\n  \"search\": \"Markup\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/activity_markup/list",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "activity_markup",
                "list"
              ]
            },
            "description": "Lists all activity markup rules with pagination and filters."
          },
          "response": []
        },
        {
          "name": "Markup Details",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": 1\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/activity_markup/details",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "activity_markup",
                "details"
              ]
            },
            "description": "Fetches details of a specific markup rule by `id`."
          },
          "response": []
        },
        {
          "name": "Update Markup Status",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": 1,\n  \"status\": 1,\n  \"is_default\": 0\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/activity_markup/status_update",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "activity_markup",
                "status_update"
              ]
            },
            "description": "Updates active status (1/0) or default flag (1/0) of a markup rule."
          },
          "response": []
        },
        {
          "name": "Delete Markup Rule",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": 1\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/activity_markup/delete",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "activity_markup",
                "delete"
              ]
            },
            "description": "Deletes a markup rule by `id`."
          },
          "response": []
        }
      ]
    },
    {
      "name": "3. Banner / Promo Images",
      "description": "Banner and promo images for the home page and inner pages. One generic table, `activity_banners`: a row is \"this image, on this `page`, in this `placement`, at this `display_order`\".\n\nIMAGE FLOW: upload first via `POST /activity/uploadImage` (request 0 below), then send the returned `fileName` as `image`. Only the bare file name is stored; a value containing `/`, `\\` or `..` is rejected. Every read returns `image_url` alongside `image`.\n\nNOTE on `page`: the list endpoints paginate with `page`, so the page-NAME filter travels as `page_name`. The column is still `page`.\n\nThese endpoints are unauthenticated, like the API Users and Activity Markup modules. See ARCHITECTURE_NOTES.md section 21.",
      "item": [
        {
          "name": "0. Upload Banner Image (get fileName)",
          "request": {
            "method": "POST",
            "header": [],
            "body": {
              "mode": "formdata",
              "formdata": [
                {
                  "key": "attachment",
                  "type": "file",
                  "src": [],
                  "description": "jpeg / jpg / png / webp only"
                }
              ]
            },
            "url": {
              "raw": "{{baseUrl}}/activity/uploadImage",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "activity",
                "uploadImage"
              ]
            },
            "description": "STEP 1 of creating a banner. The existing shared image-upload endpoint -- not banner-specific. Multipart form-data, field name `attachment`.\n\nReturns `{ \"status\": true, \"fileName\": \"img_1749466103251.jpg\" }`. Send that `fileName` as the `image` field on /banner/create or /banner/update -- the banner table stores the bare file name only, never a path or a URL."
          },
          "response": []
        },
        {
          "name": "1. Create Banner",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"banner_type\": \"banner\",\n  \"page\": \"home\",\n  \"placement\": \"top_slider\",\n  \"image\": \"img_1749466103251.jpg\",\n  \"title\": \"Summer in Dubai\",\n  \"sub_title\": \"Up to 30% off on desert safaris\",\n  \"alt_text\": \"Desert safari banner\",\n  \"display_order\": 1,\n  \"status\": 1\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/banner/create",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "banner",
                "create"
              ]
            },
            "description": "Creates a banner / promo row. `image` is required and must be the bare file name returned by /activity/uploadImage. `banner_type` is `banner` (hero/slider) or `promo` (tile); `page` and `placement` are free-form, so a new page or slot needs no migration. Returns the created row including `image_url`."
          },
          "response": []
        },
        {
          "name": "2. Update Banner",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": 1,\n  \"title\": \"Summer in Dubai - extended\",\n  \"display_order\": 2,\n  \"image\": \"img_1749466200000.jpg\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/banner/update",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "banner",
                "update"
              ]
            },
            "description": "Partial update -- only the fields present in the body are written. Allowed: banner_type, page, placement, image, title, sub_title, alt_text, display_order, status. Sending `image` replaces the file name; the old file is left on disk on purpose."
          },
          "response": []
        },
        {
          "name": "3. List Banners (admin)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"page\": 1,\n  \"limit\": 10,\n  \"status\": 1,\n  \"banner_type\": \"banner\",\n  \"page_name\": \"home\",\n  \"placement\": \"top_slider\",\n  \"search\": \"Dubai\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/banner/list",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "banner",
                "list"
              ]
            },
            "description": "Paginated admin listing. Does NOT filter by status by default -- hidden rows must be visible to the admin. All filters are optional; drop the ones you do not need. Ordered by display_order ASC, id DESC. Returns `totalRecords`, `page`, `limit`."
          },
          "response": []
        },
        {
          "name": "4. Banner Details",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": 1\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/banner/details",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "banner",
                "details"
              ]
            },
            "description": "Single row by id, including `image_url`."
          },
          "response": []
        },
        {
          "name": "5. Update Banner Status",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": 1,\n  \"status\": 0,\n  \"display_order\": 3\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/banner/status_update",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "banner",
                "status_update"
              ]
            },
            "description": "Inline grid toggle. Accepts `status` and/or `display_order` -- re-ordering a slider is the other one-field edit the admin grid makes. At least one of the two is required."
          },
          "response": []
        },
        {
          "name": "6. Delete Banner",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"id\": 1\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/banner/delete",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "banner",
                "delete"
              ]
            },
            "description": "Deletes the row only. The uploaded file stays in the uploads tree: the same file name may be referenced by another row or a cached page."
          },
          "response": []
        },
        {
          "name": "7. Front Banners (public)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"page_name\": \"home\",\n  \"placement\": \"top_slider\",\n  \"banner_type\": \"banner\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/banner/front_list",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "banner",
                "front_list"
              ]
            },
            "description": "Public read for the website. Live rows only (`status = 1`) for one page, ordered by display_order ASC, id DESC. `placement` and `banner_type` are optional narrowing filters -- omit them to get the whole page.\n\nReturns the rows twice: flat in `data`, and in `grouped` keyed by placement, e.g. { \"top_slider\": [...], \"mid_strip\": [...] }, so each slot renders without client-side filtering. Unpaginated by design."
          },
          "response": []
        }
      ]
    },
    {
      "name": "4. Out API",
      "item": [
        {
          "name": "1. Login (send OTP)",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Step 1 of 2: this does NOT log you in. It mails a 6-digit code and",
                  "// returns a 5-minute otp_token that only /xApi/verify_otp accepts.",
                  "const body = pm.response.json();",
                  "",
                  "pm.test('OTP sent', function () {",
                  "    pm.response.to.have.status(200);",
                  "    pm.expect(body.status).to.eql(true);",
                  "    pm.expect(body.otp_token).to.be.a('string').and.not.empty;",
                  "});",
                  "",
                  "pm.test('no access token is issued at step 1', function () {",
                  "    pm.expect(body.token).to.be.undefined;",
                  "});",
                  "",
                  "if (body && body.otp_token) {",
                  "    pm.collectionVariables.set('otp_token', body.otp_token);",
                  "    console.log('otp_token stored. Code sent to ' + body.email +",
                  "                ' \u2014 put it in the otp_code variable, then run \"2. Verify OTP\".');",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"username\": \"{{xapi_username}}\",\n  \"password\": \"{{xapi_password}}\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/xApi/login",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "xApi",
                "login"
              ]
            },
            "description": "Step 1 of 2. Verifies the password, writes a 6-digit code to activity_users.mail_otp and emails it to the address on the row.\n\n`username` accepts EITHER the partner's email OR their `user_reference_id`.\n\nReturns `otp_token` (5 min) and a masked email \u2014 no access token and no profile; neither is owed to a caller who has cleared only one of two factors.\n\nErrors: 401 for any credential failure (one message, so this cannot be used to enumerate partners), 403 if the row has no email address, 502 if the mail could not be sent (the pending code is cleared in that case).\n\nThe test script saves otp_token into {{otp_token}}."
          },
          "response": []
        },
        {
          "name": "2. Verify OTP",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// Step 2 of 2: exchanges otp_token + the emailed code for the 24h",
                  "// access token, clears mail_otp and stamps last_login.",
                  "const body = pm.response.json();",
                  "",
                  "pm.test('login complete', function () {",
                  "    pm.response.to.have.status(200);",
                  "    pm.expect(body.status).to.eql(true);",
                  "    pm.expect(body.token).to.be.a('string').and.not.empty;",
                  "});",
                  "",
                  "if (body && body.token) {",
                  "    pm.collectionVariables.set('xapi_token', body.token);",
                  "    pm.collectionVariables.set('otp_token', '');",
                  "    console.log('xapi_token stored, expires_in=' + body.expires_in +",
                  "                ', last_login=' + (body.data && body.data.last_login));",
                  "}"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"otp_token\": \"{{otp_token}}\",\n  \"otp\": \"{{otp_code}}\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/xApi/verify_otp",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "xApi",
                "verify_otp"
              ]
            },
            "description": "Step 2 of 2. Put the 6-digit code from the email into the {{otp_code}} collection variable first (in development you can also read it from activity_users.mail_otp).\n\nOn success: returns the 24h Bearer token and the partner profile, clears mail_otp and stamps last_login.\n\nThe code is one-shot. 3 wrong attempts burn it \u2014 after that even the correct code fails and you must run \"1. Login\" again. Wrong, expired, already-used and never-issued all return the same 401.\n\nThe test script saves token into {{xapi_token}}."
          },
          "response": []
        },
        {
          "name": "3. Activity List",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{xapi_token}}",
                "type": "text",
                "description": "From \"2. Verify OTP\". X-API-KEY is no longer accepted (2026-08-31)."
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"keyword\": \"Zoo\",\n  \"country\": \"Singapore\",\n  \"city\": \"Singapore\",\n  \"from_date\": \"2026-09-01\",\n  \"guest_currency\": \"INR\",\n  \"page\": 1,\n  \"limit\": 20\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/xApi/activity_list",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "xApi",
                "activity_list"
              ]
            },
            "description": "Requires `Authorization: Bearer {{xapi_token}}` \u2014 run \"1. Login\" then \"2. Verify OTP\" first.\nThe old `X-API-KEY` header is rejected as of 2026-08-31.\n\nFetches list of activities for external partners. Authenticates via X-API-KEY header and applies matching activity_markup rules.\n\nAUTH IS OPTIONAL as of 2026-09-03: send the Bearer token to be quoted your own markup, or omit the Authorization header entirely to get the public answer priced with the DEFAULT markup rule. A token that is SENT but is expired or invalid is still a 401 \u2014 it is not downgraded to anonymous. `api_user_ref` appears on the response only when a token was accepted, which is how you can tell yours arrived."
          },
          "response": []
        },
        {
          "name": "4. Get Activity By Id",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{xapi_token}}",
                "type": "text",
                "description": "From \"2. Verify OTP\". X-API-KEY is no longer accepted (2026-08-31)."
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"activity_id\": 123,\n  \"daydate\": \"2026-09-10\",\n  \"guest_currency\": \"INR\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/xApi/get_activity_by_id",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "xApi",
                "get_activity_by_id"
              ]
            },
            "description": "Detail lookup for ONE activity. Requires `Authorization: Bearer {{xapi_token}}` \u2014 run \"1. Login\" then \"2. Verify OTP\" first.\n\n`activity_id` is REQUIRED and must be numeric; an absent or non-numeric id is a 400 `bad_request` (it is not treated as \"no filter\").\n\nOptional: `daydate` (or `from_date`, both accepted) defaults to today and is the date prices, inventory and time slots are resolved for; `guest_currency` defaults to the partner's own currency; `country`, `city`, `keyword`, `is_combo` are extra filters.\n\nReturns the same envelope as \"3. Activity List\" with `cmd: \"x_get_activity_by_id\"` and a `data` array of 0 or 1 items \u2014 an array, so one parser works for both routes. Prices carry `activity_markup` rules, the same single markup layer the list route uses.\n\nDetail semantics: an activity that is closed or expired for the requested date is still returned, carrying `expired` / `booking_closed` / `available_quantity`. It comes back with an empty `data` only when it has no sellable inventory for that date at all \u2014 try another `daydate` before concluding the id is wrong.\n\nOwn inventory (ACT) only \u2014 GlobalTix has no per-product detail path. A partner whose `allowed_sources` has no `ACT` gets 403 `source_not_granted` rather than an empty result.\n\nAUTH IS OPTIONAL as of 2026-09-03: send the Bearer token to be quoted your own markup, or omit the Authorization header entirely to get the public answer priced with the DEFAULT markup rule. A token that is SENT but is expired or invalid is still a 401 \u2014 it is not downgraded to anonymous. `api_user_ref` appears on the response only when a token was accepted, which is how you can tell yours arrived."
          },
          "response": []
        },
        {
          "name": "5. Get Activities By Ids",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{xapi_token}}",
                "type": "text",
                "description": "OPTIONAL. Omit the header entirely for the public answer at default markup."
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"ids\": [123, 456, 789],\n  \"travel_date\": \"2026-09-10\",\n  \"guest_currency\": \"INR\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/xApi/get_activity_by_ids",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "xApi",
                "get_activity_by_ids"
              ]
            },
            "description": "Batch lookup \u2014 the Out API port of /v1/get_activity_by_ids (holiday cross-sell).\n\n`ids` is REQUIRED: an array, a single id, or a comma-separated string. All values must be numeric \u2014 a non-numeric entry is a 400 naming it, not a silently dropped id. Maximum 100 ids per call; beyond that is a 400 `too_many_ids` rather than a silent truncation. Duplicates are collapsed before the cap is applied.\n\nOptional: `travel_date` (also accepted as `daydate` or `from_date`) defaults to today and is the date prices, inventory and time slots resolve for; `guest_currency` defaults to the partner's own currency.\n\nAUTH IS OPTIONAL: send the Bearer token to be quoted your own markup, or omit the header for the public answer priced with the DEFAULT rule. A token that is SENT but expired or invalid is still a 401.\n\nThe response carries `requested_ids` and `missing_ids` alongside `data`. An id lands in `missing_ids` when there is no such activity, it is inactive, it is a MEAL activity (excluded on this route, matching /v1 \u2014 unlike the single-id route), it does not run on that weekday, it is country-locked away from your account, or it has no sellable inventory for that date. If an id you expect is missing, try another `travel_date` before concluding it does not exist.\n\nOwn inventory (ACT) only \u2014 GlobalTix has no per-product detail path."
          },
          "response": []
        },
        {
          "name": "6. Get Time Slots",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{xapi_token}}",
                "type": "text",
                "description": "OPTIONAL. Omit the header entirely for the public answer at default markup."
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"ref_id\": 123,\n  \"ref_date\": \"2026-09-10\",\n  \"guest_currency\": \"INR\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/xApi/get_time_slots",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "xApi",
                "get_time_slots"
              ]
            },
            "description": "Time slots for ONE activity \u2014 the Out API port of /web/get_time_slots.\n\n`ref_id` is REQUIRED and is the ACTIVITY id (not a slot id). Unlike /web, an empty body is a 400 rather than every time slot in the system.\n\n`ref_date` is optional and matches `inventory_date` exactly, in YYYY-MM-DD. Omit it and you get every slot from today onward \u2014 /web has no lower bound and returns past dates too. `guest_currency` defaults to the partner's own currency.\n\nActivity slots only: `ref_from` may be omitted or set to \"activity\". \"sub-activity\" is a 400 \u2014 sub-activities are not part of the Out API contract, so a partner has no way to resolve one of their ids.\n\nAUTH IS OPTIONAL: send the Bearer token to be quoted your own markup, or omit the header for the public answer priced with the DEFAULT rule. A token that is SENT but expired or invalid is still a 401.\n\nEach row carries `currency` \u2014 what that row's prices are ACTUALLY quoted in \u2014 alongside `guest_currency` and `price_converted`. They differ only when a currency conversion failed, in which case the row stays in the activity's own currency rather than being mislabelled. `activity_id` echoes the owning activity."
          },
          "response": []
        },
        {
          "name": "7. Get Transfers By Ids",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{xapi_token}}",
                "type": "text",
                "description": "OPTIONAL. Omit the header entirely for the public answer at default markup."
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"ids\": [12, 34],\n  \"travel_date\": \"2026-09-10\",\n  \"guest_currency\": \"INR\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/xApi/get_transfer_by_ids",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "xApi",
                "get_transfer_by_ids"
              ]
            },
            "description": "Batch lookup for TRANSPORT PACKAGES \u2014 the Out API port of the transport service's /v1/get_transfer_holiday_ids (holiday transfer cross-sell). The transport twin of \"Get Activities By Ids\", with the same id rules.\n\n`ids` is REQUIRED: an array, a single id, or a comma-separated string. All values must be numeric \u2014 a non-numeric entry is a 400 naming it, not a silently dropped id. Maximum 100 ids per call; beyond that is a 400 `too_many_ids` rather than a silent truncation. Duplicates are collapsed.\n\n`travel_date` is OPTIONAL here (unlike on the activity routes; `daydate` and `from_date` are also accepted). Send it and prices, vehicle rates, time slots and the booking cutoff resolve against that date's inventory. Omit it and the packages answer at base price with no `available_time_slots`. A date already past returns an empty list; a date that cannot be parsed is a 400. `guest_currency` defaults to the partner's own currency.\n\nAUTH IS OPTIONAL: send the Bearer token to be quoted your own markup, or omit the header for the public answer priced with the DEFAULT rule. A token that is SENT but expired or invalid is still a 401.\n\nA package that is CLOSED for the requested date is still RETURNED, carrying `booking_closed: 1` \u2014 render it unavailable rather than dropping it. So `missing_ids` means exactly one of: no such package, it is inactive, or it is country-locked away from your account.\n\nEach row carries `price` / `sale_price` / `sic_price` and a `vehicles_price[]` of per-vehicle rates, all in `guest_currency` \u2014 unless conversion failed, in which case the row keeps its own `currency` and says so with `price_converted: false`. `inventory_source` tells you whether the row was priced from the date's inventory (`inventory`) or the package's base configuration (`base`). `imgurl` at the top level is the base for `package_image`; rows also carry an absolute `package_image_url` when the server has that base configured.\n\nOwn inventory (ACT) only."
          },
          "response": []
        },
        {
          "name": "8. Get Transport Supplier Inventory (buy-side)",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{xapi_token}}",
                "type": "text",
                "description": "REQUIRED on this route. Unlike the catalogue routes there is no anonymous path \u2014 a missing or invalid token is a 401."
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"transport_id\": 12,\n  \"travel_date\": \"2026-09-10\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/xApi/get_transport_supplier_inventory_booking",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "xApi",
                "get_transport_supplier_inventory_booking"
              ]
            },
            "description": "BUY-SIDE route \u2014 the Out API port of the transport service's /v1/get_transport_supplier_inventory_booking. It answers \"which supplier can serve this transport package on this date, and what do they charge US\".\n\nTHIS ROUTE IS THE ODD ONE OUT ON /xApi/*, in three ways:\n\n1. THE BEARER TOKEN IS MANDATORY. There is no anonymous path, unlike every catalogue route in this folder. A missing header is a 401, not a public answer.\n2. NO MARKUP IS APPLIED. These are PURCHASE prices, not selling prices. Do not quote them to a customer.\n3. NO CURRENCY CONVERSION. Every row is in ITS OWN supplier contract's currency \u2014 read `currency` PER ROW. Two rows in one response can legitimately be in different currencies. The response states `priced_in: \"supplier_currency\"` to make this impossible to miss.\n\n`transport_id` (numeric) and `travel_date` (YYYY-MM-DD) are REQUIRED; either missing or malformed is a 400. `supplier_id` is an optional narrowing filter \u2014 send it invalid and you get a 400 rather than a silently widened answer.\n\nRows come back CHEAPEST FIRST (`sic_price ASC`), which is the sourcing order \u2014 the first row is the supplier to buy from. Each row carries the supplier's contact details, `sic_price`, the per-vehicle rate card in `vehicles_price[]`, and `cancellation_policy` with its `items[]` (already grouped; the underlying join returns one row per policy item)."
          },
          "response": []
        },
        {
          "name": "9. Create Booking",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{xapi_token}}",
                "type": "text",
                "description": "REQUIRED. The token also decides WHO IS CHARGED - there is no anonymous path and no body-supplied agent."
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"reference_type\": 1,\n  \"reference_id\": 4,\n  \"from_date\": \"2026-09-10\",\n  \"to_date\": \"2026-09-10\",\n  \"title\": \"Safari world with Marine Park + Lunch Included\",\n  \"city\": \"Bangkok\",\n  \"country\": \"Thailand\",\n  \"currency\": \"INR\",\n  \"time_slot\": 133267,\n  \"time_from\": \"10:00:00\",\n  \"time_to\": \"18:00:00\",\n  \"transfer_mode\": \"3\",\n  \"pickup_location\": \"Bangkok\",\n  \"drop_location\": \"Bangkok\",\n  \"no_of_travellers\": {\n    \"room\": 1,\n    \"adult\": 1,\n    \"child\": 0,\n    \"infant\": 0,\n    \"travellers\": 1\n  },\n  \"guest_info\": {\n    \"first_name\": \"Gaurav\",\n    \"last_name\": \"Sharma\",\n    \"email\": \"gsharma.btp@gmail.com\",\n    \"contact_no\": \"8952072758\"\n  },\n  \"price\": 1349.95,\n  \"markup\": 0,\n  \"discount\": 0,\n  \"total_price\": 1349.95,\n  \"reference_object\": {\n    \"id\": \"4\",\n    \"source\": \"BW\",\n    \"activity_date\": \"2026-09-10\",\n    \"inventory_source\": \"time_slot\",\n    \"default_category\": 8,\n    \"selected_category_id\": 8,\n    \"categories\": [\n      {\n        \"category_id\": 8,\n        \"category_name\": \"Standard\",\n        \"available_quantity\": 10,\n        \"selected\": true,\n        \"category_qty\": 1\n      }\n    ],\n    \"time_slots\": [\n      {\n        \"id\": 133267,\n        \"from_time\": \"10:00:00\",\n        \"to_time\": \"13:00:00\",\n        \"inventory_date\": \"2026-09-10\",\n        \"available_quantity\": 40\n      }\n    ],\n    \"isSelectedTimeSlotObject\": {\n      \"id\": 133267,\n      \"inventory_date\": \"2026-09-10\",\n      \"available_quantity\": 40\n    },\n    \"no_of_travellers\": {\n      \"room\": 1,\n      \"adult\": 1,\n      \"child\": 0,\n      \"infant\": 0,\n      \"travellers\": 1\n    }\n  }\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/xApi/create_booking",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "xApi",
                "create_booking"
              ]
            },
            "description": "Creates an activity booking. THE FIRST WRITE ROUTE ON /xApi/* - everything else in this folder reads.\nIt takes the SAME body /web/create_booking takes, so an existing booking payload can be posted unchanged.\n\nTHE BEARER TOKEN IS IDENTITY, NOT JUST PERMISSION. The authenticated partner becomes the booking's agent AND is the wallet that gets charged:\n  - fd_activities_booking.agent_id = your activity_users.id\n  - staff_id = NULL\n  - `agent_id`, `staff_id` and `source` sent in the body are IGNORED. You cannot book against another account.\n\nREQUIRED\n  reference_type    must be 1 (only activity bookings exist here)\n  reference_id      the activity id (reference_object.id is accepted instead)\n  from_date         YYYY-MM-DD (reference_object.activity_date is accepted instead)\n  no_of_travellers  must total at least 1 unit - see UNITS below\n  total_price       > 0, and must be covered by your wallet balance\n\nUNITS = no_of_travellers.travellers, or adult + child when it is absent. INFANTS DO NOT CONSUME A UNIT.\n\nWHICH INVENTORY ROW IS DECREMENTED comes from reference_object.inventory_source - the field our own activity_list/get_activity_by_id already returns on every item. Send reference_object back as you received it (plus the front-end selections) and this resolves itself. Exactly ONE row moves per booking:\n\n  inventory_source    row decremented                      id read from\n  ------------------  -----------------------------------  ------------------------------------------\n  time_slot           time_slot.available_quantity         time_slot / time_slot_id, else\n                                                           reference_object.isSelectedTimeSlotObject.id,\n                                                           else reference_object.time_slots[0].id\n  category            activity_inventory.available_quantity category_id, else\n                                                           reference_object.selected_category_id, else\n                                                           the categories[] entry with selected: true,\n                                                           else reference_object.default_category\n  inventory           activity_inventory (category_id 0)   -\n  base / master       activity.qty                         -\n\nNOTE: reference_object.time_slot is a 0/1 FLAG (\"this activity sells by slot\"), not a slot id. The slot id is the TOP-LEVEL time_slot, or the ids inside reference_object.time_slots / isSelectedTimeSlotObject.\n\nONE TRANSACTION: BOOKING + INVENTORY + WALLET\n  - your wallet row is locked and checked first, then the units are taken from the inventory row, then the booking is written, then the wallet is debited.\n  - all of it commits together or none of it happens. There is no state where you hold a booking nobody was charged for, or seats held for a booking that failed.\n  - the charge is a new row in fdk_activity.website_accounts carrying your new running balance; the reply's data.wallet_balance is that balance.\n  - a NULL inventory quantity means UNAVAILABLE, not unlimited.\n\nERRORS (all 400 unless noted)\n  unauthorized (401)      missing/invalid Bearer token\n  bad_request             a required field is missing or malformed\n  insufficient_balance    wallet cannot cover total_price\n  duplicate_booking       same activity + amount within 1 minute; the reply carries the original booking_ref_no\n  no_inventory            no inventory row for the resolved target, or inventory_source is \"category\" with no category id\n  no_time_slot            the slot does not exist for this activity on this date, or inventory_source is \"time_slot\" with no slot id\n  insufficient_inventory  fewer units available than requested; the reply carries available_quantity\n\nA successful reply carries data.booking_id, data.booking_ref_no (ACT + 6 chars), data.units, data.inventory_source and data.wallet_balance. A confirmation email with the PDF voucher is sent to the partner address on file."
          },
          "response": []
        },
        {
          "name": "10. Wallet Balance",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{xapi_token}}",
                "type": "text",
                "description": "REQUIRED, and it is the ONLY thing that selects whose balance is returned."
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"amount\": 1349.95\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/xApi/wallet_balance",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "xApi",
                "wallet_balance"
              ]
            },
            "description": "Your wallet balance, and optionally whether it covers a booking you are about to make.\n\nTHE TOKEN PICKS THE WALLET. There is no user_id in the body and sending one has no effect - you can only ever read your own balance.\n\nBODY (optional)\n  { \"amount\": 1349.95 }   adds sufficient / shortfall / requested_amount to the reply.\n  {}                      just the balance.\n\nREPLY\n  data.available_balance  your current balance\n  data.has_wallet         false (with a 0 balance) if no wallet account exists yet - this is a SUCCESS, not an error\n  data.as_of              when the balance last moved, not \"now\"\n  data.sufficient         only when amount was sent; uses the same >= check create_booking uses, so an exact balance passes both\n  data.shortfall          how much more you need, 0 when sufficient\n\nThis is a plain read: it takes no lock, so calling it never blocks or delays a booking. It is also not a reservation - a balance that was sufficient a second ago can be spent by another booking of yours before you post create_booking, which is why create_booking re-checks it under a lock and can still answer insufficient_balance.\n\nTransaction history is deliberately NOT returned: this ledger is shared with flights, hotels, visas and payment-gateway settlement, so only the balance and its timestamp leave the building."
          },
          "response": []
        },
        {
          "name": "11. Bookings List",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{xapi_token}}",
                "type": "text",
                "description": "REQUIRED, and it is the ONLY thing that decides which rows come back."
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"status\": \"CONFIRMED\",\n  \"from_date\": \"2026-09-01\",\n  \"to_date\": \"2026-09-30\",\n  \"date_type\": \"booking_date\",\n  \"search\": \"\",\n  \"page\": 1,\n  \"limit\": 20\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/xApi/bookings_list",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "xApi",
                "bookings_list"
              ]
            },
            "description": "Your own bookings, newest first.\n\nTHE TOKEN IS THE WHOLE SCOPE. There is no agent_id in the body; sending one has no effect. You can only ever see bookings you made through this API.\n\nFILTERS (all optional)\n  booking_ref_no   exact match\n  reference_id     activity id\n  status           PENDING | CONFIRMED | CANCELLED\n  from_date/to_date  inclusive, YYYY-MM-DD\n  date_type        \"booking_date\" filters on when it was booked (default), \"travel_date\" on the travel date (the older \"booking\" / \"travel\" spellings still work)\n  search           matches booking ref, title, confirmation no, and the guest name / email / contact\n  sort_order       ASC | DESC (default DESC, by id)\n  page, limit      limit defaults to 20 and is capped at 100\n\nEach row carries booking_status (PENDING / CONFIRMED / CANCELLED) alongside the raw status and ops_confirmed flags, plus pagination { total, page, limit, total_pages }.\n\nNOT RETURNED: purchase prices, supplier contracts and contact details, payout and ops-workflow flags, and reference_object (which nests supplier pricing inside it). This is the sell side of your own booking."
          },
          "response": []
        },
        {
          "name": "12. Booking Details",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{xapi_token}}",
                "type": "text",
                "description": "REQUIRED, and it is the ONLY thing that decides which rows come back."
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"booking_ref_no\": \"ACTAB12CD\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/xApi/booking_details",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "xApi",
                "booking_details"
              ]
            },
            "description": "One booking, in full.\n\nSend booking_ref_no OR booking_id - exactly one is required.\n\nReturns everything the list returns plus reference_object (the activity snapshot you booked against), confirmed_pax, the addon/combo cancellation objects, support_info and driver_details.\n\nA booking that does not exist and a booking belonging to another partner return the IDENTICAL 404 (\"Booking not found\"). That is deliberate: anything else would let you probe for other partners' references.\n\nreference_object is returned SANITISED: supplier contracts, supplier contact details, purchase prices, net (org_*) rates and currency-conversion blocks are stripped at every depth, including inside categories[] and time_slots[]. Everything else - ids, sell prices, mrp, availability, the price breakdown - comes through unchanged.\n\nSame exclusions as the list otherwise: no purchase prices, no supplier details."
          },
          "response": []
        },
        {
          "name": "13. Transactions List",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{xapi_token}}",
                "type": "text",
                "description": "REQUIRED, and it is the ONLY thing that decides which rows come back."
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"drcr\": \"DR\",\n  \"from_date\": \"2026-09-01\",\n  \"to_date\": \"2026-09-30\",\n  \"page\": 1,\n  \"limit\": 20\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/xApi/transactions_list",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "xApi",
                "transactions_list"
              ]
            },
            "description": "Your wallet statement - the ledger behind the number wallet_balance returns, and the rows create_booking writes.\n\nScoped by your token alone; there is no user_id in the body.\n\nFILTERS (all optional)\n  account_type     e.g. activity_booking\n  drcr             DR (money out) | CR (money in)\n  booking_id       the numeric booking id\n  reference_id     the booking reference (ACT...)\n  from_date/to_date  on the transaction date, YYYY-MM-DD\n  page, limit      limit defaults to 20 and is capped at 100\n\nEach row: id, booking_id, booking_type, reference_id, account_type, drcr, amount, available_balance (your running balance after that transaction), narration, createdAt.\n\namount is always POSITIVE - drcr carries the direction. Gateway, credit-reversal and OTC columns on this shared ledger are not returned."
          },
          "response": []
        },
        {
          "name": "14. Forgot Password",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"username\": \"partner@example.com\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/xApi/forgot_password",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "xApi",
                "forgot_password"
              ]
            },
            "description": "Step 1 of 2 of a password reset. PUBLIC - no token, because a partner who cannot log in has none.\n\nMails a 6-digit code to the address on file and returns the reset_token that step 2 consumes. Nothing changes here: your current password keeps working until reset_password succeeds.\n\nusername is matched against BOTH email and user_reference_id, exactly as login matches it.\n\nTHE ANSWER IS THE SAME WHETHER OR NOT THE ACCOUNT EXISTS - same 200, same message, a reset_token either way. That is deliberate: otherwise this route could be used to find out which addresses are partners. So a 200 here does NOT confirm an account exists, and if no mail arrives the likely causes are: no such account, no email on file, the account is inactive or unverified, or the mail could not be sent.\n\nThe code goes to the registered address and nowhere else - there is no way to nominate a recipient.\n\nNOTE: the code shares a column with the login OTP, so requesting a reset invalidates a login code you were already waiting on (and vice versa)."
          },
          "response": []
        },
        {
          "name": "15. Reset Password",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"reset_token\": \"{{reset_token}}\",\n  \"otp\": \"123456\",\n  \"new_password\": \"your-new-password\",\n  \"confirm_password\": \"your-new-password\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/xApi/reset_password",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "xApi",
                "reset_password"
              ]
            },
            "description": "Step 2 of 2. PUBLIC - the reset_token and the emailed code ARE the credentials.\n\nREQUIRED: reset_token (from forgot_password, valid 15 minutes), otp (the 6-digit code), new_password.\nOPTIONAL: confirm_password - if sent it must match.\n\nPASSWORD RULES\n  at least 8 characters\n  at most 72 BYTES (bcrypt silently truncates past that, so longer is refused rather than cut)\n  must differ from your current password\n\n3 wrong codes burn it and you start again at forgot_password. The code is one-shot: it is cleared on success too.\n\nNO ACCESS TOKEN IS RETURNED. Proving you can read the mailbox is not the same as signing in - call /xApi/login afterwards.\n\nA login otp_token or an access token will NOT work here, and a token issued for an identifier that does not exist fails with the same \"Invalid or expired reset code\" as a wrong code."
          },
          "response": []
        },
        {
          "name": "16. Get User Details",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{xapi_token}}",
                "type": "text",
                "description": "REQUIRED, and it is the whole of the addressing - there is no id in the body."
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{}"
            },
            "url": {
              "raw": "{{baseUrl}}/xApi/get_user_details",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "xApi",
                "get_user_details"
              ]
            },
            "description": "Your own profile. The token is the whole of the addressing - there is no id in the body, so you can only ever read yourself.\n\nReturns: id, user_reference_id, role_id, name, company_name, email, mobile, phone, dialcode, image, address, address_2, city, state_id, state_name, country_id, country_name, zipcode, currency_code, website, gst_no, pan_no, gst_doc, pan_doc, prefix, credit_limit, verified, status, pan_verified, last_login, createdAt, updatedAt.\n\nNOT returned: password and the login code (obviously), and the wallet columns - your balance is /xApi/wallet_balance, and a stale copy on a profile is worse than none."
          },
          "response": []
        },
        {
          "name": "17. Update User Details",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{xapi_token}}",
                "type": "text",
                "description": "REQUIRED, and it is the whole of the addressing - there is no id in the body."
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Test Partner\",\n  \"company_name\": \"Test Partner Ltd\",\n  \"mobile\": \"9999999999\",\n  \"address\": \"12 Sukhumvit Road\",\n  \"city\": \"Bangkok\",\n  \"country_name\": \"Thailand\",\n  \"zipcode\": \"10110\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/xApi/update_user_details",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "xApi",
                "update_user_details"
              ]
            },
            "description": "Updates your own profile. The row is addressed by your token, never by anything in the body.\n\nWRITEABLE: name, company_name, mobile, phone, dialcode, image, address, address_2, city, state_id, state_name, country_id, country_name, zipcode, website, gst_no, pan_no, gst_doc, pan_doc.\n\nNOT WRITEABLE - sending these is harmless but has no effect:\n  password                       use /xApi/reset_password\n  email, user_reference_id       login identifiers; changing an email without proving you control the new mailbox would hand over the account, since the login code goes there\n  status, verified, role_id,\n  credit_limit, allowed_sources  ours to grant\n  wallet, deposit, credit, otc_* money moves through the wallet ledger, not a profile form\n  pan_verified                   the result of our check on your document\n\nAnything outside the writeable list is IGNORED rather than rejected, so posting the whole profile object back works. The response lists updated_fields and ignored_fields so you can see exactly what moved, and data is the row READ BACK from the database, not your input echoed.\n\nSend at least one writeable field or you get a 400. Strings are trimmed, empty strings become NULL (except name, which cannot be blanked), state_id/country_id must be integers."
          },
          "response": []
        },
        {
          "name": "18. Change Password",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              },
              {
                "key": "Authorization",
                "value": "Bearer {{xapi_token}}",
                "type": "text",
                "description": "REQUIRED. This route is for the partner who knows their password; the one who does not uses forgot_password."
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"current_password\": \"your-current-password\",\n  \"new_password\": \"your-new-password\",\n  \"confirm_password\": \"your-new-password\"\n}"
            },
            "url": {
              "raw": "{{baseUrl}}/xApi/change_password",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "xApi",
                "change_password"
              ]
            },
            "description": "Changes your password while signed in. Your CURRENT password is the proof, so there is no emailed code here - if you cannot supply it, use forgot_password / reset_password instead.\n\nREQUIRED: current_password, new_password.\nOPTIONAL: confirm_password - if sent it must match.\n\nPASSWORD RULES (same as a reset)\n  at least 8 characters\n  at most 72 BYTES (bcrypt silently truncates past that, so longer is refused rather than cut)\n  must differ from your current password\n\nA wrong current_password is a 401 and changes nothing. Any login or reset code you were waiting on is cancelled by the change.\n\nIMPORTANT: access tokens issued BEFORE the change keep working until they expire (up to 24h) - these are stateless JWTs with no revocation list. If you are changing the password because it may have leaked, ask your account manager to deactivate the account as well."
          },
          "response": []
        },
        {
          "name": "19. Search Autofill",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"keyword\": \"Bangkok\",\n    \"limits\": {\n        \"countries\": 5,\n        \"cities\": 10,\n        \"activities\": 10\n    }\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/xApi/autofill",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "xApi",
                "autofill"
              ]
            },
            "description": "Search typeahead for the front-end search box. PUBLIC - no Bearer token, and no price on the response, so it needs no markup pass.\n\nReturns three buckets in `data`:\n  countries  - countries we sell in whose name matches\n  cities     - cities matching the keyword OR sitting inside a matching country (this is what makes \"Thailand\" return every Thai city). Each row carries matched_on: \"city\" | \"country\".\n  activities - activities matching on name, type, city or country. Name matches rank above location-only matches.\n\nBody (all optional): keyword, limit (caps all three buckets), or limits: { countries, cities, activities } to size them separately.\n\nA keyword shorter than XAPI_AUTOFILL_MIN_KEYWORD (default 2) returns a 200 with empty buckets, not a 400 - the route is called on every keystroke. Try \"Bangkok\" (a city) and \"Thailand\" (a country) to see both shapes."
          },
          "response": []
        }
      ],
      "description": "Partner-facing endpoints. Run Login then Verify OTP; the requests below send Authorization: Bearer {{xapi_token}}."
    }
  ]
}
