{
  "info": {
    "_postman_id": "35b91070-aae9-43fb-a118-536cb0cf6775",
    "name": "FX Bridge Rates API",
    "description": "Central bank exchange rates by source and date.\n\n**Setup**\n1. Create an API key in the FX Bridge portal (menu: *Rates API*; Enterprise plan), or run the requests in *4. Portal – API key management*.\n2. Paste the key into the collection variable `apiKey` (Variables tab). Keys only work from the IPs in their allowlist: add the public IP of the machine running Postman to the key in the portal (Rates API → IPs).\n3. Adjust `rateDate` / `source` as needed and send.\n\n**Rate direction**: `rate` = value of 1 unit of `currency` in `base_currency` (the Oracle GL daily rate with FROM = currency, TO = base_currency). `inverse_rate` is the opposite direction.\n\nLimits: 120 requests/minute per key. Docs: https://fxbridge.experteamsolutions.com/docs#rates-api",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "apikey",
    "apikey": [
      {
        "key": "key",
        "value": "X-API-Key",
        "type": "string"
      },
      {
        "key": "value",
        "value": "{{apiKey}}",
        "type": "string"
      },
      {
        "key": "in",
        "value": "header",
        "type": "string"
      }
    ]
  },
  "variable": [
    {
      "key": "baseUrl",
      "value": "https://api-fxbridge.experteamsolutions.com/api/v1"
    },
    {
      "key": "apiKey",
      "value": "",
      "type": "secret",
      "description": "fxr_... key from the portal (Rates API page)"
    },
    {
      "key": "source",
      "value": "ecb"
    },
    {
      "key": "rateDate",
      "value": "2026-09-18",
      "description": "YYYY-MM-DD or latest"
    },
    {
      "key": "email",
      "value": "",
      "description": "Portal login (only for the key-management folder)"
    },
    {
      "key": "password",
      "value": "",
      "type": "secret"
    },
    {
      "key": "serverIp",
      "value": "",
      "description": "Public IP address of the server that will call the API (required IP allowlist, e.g. 198.51.100.10)"
    },
    {
      "key": "accessToken",
      "value": "",
      "type": "secret",
      "description": "Set automatically by Login"
    },
    {
      "key": "apiKeyId",
      "value": "",
      "description": "Set automatically by Create API key"
    }
  ],
  "item": [
    {
      "name": "1. Rates API",
      "item": [
        {
          "name": "List sources",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/public/sources",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "public",
                "sources"
              ]
            },
            "description": "Sources in your plan with base currency, rate types and latest available date."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 200', () => pm.response.to.have.status(200));",
                  "pm.test('Sources listed', () => pm.expect(pm.response.json().data).to.be.an('array').that.is.not.empty);"
                ]
              }
            }
          ]
        },
        {
          "name": "Rates by source and date",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/public/rates/{{source}}/{{rateDate}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "public",
                "rates",
                "{{source}}",
                "{{rateDate}}"
              ]
            },
            "description": "All rates `source` published for `rateDate`. Returns 404 on weekends/holidays unless `fallback=true`."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('Rates returned', () => pm.expect(body.data.rates).to.be.an('array').that.is.not.empty);",
                  "pm.test('Each rate has rate and inverse_rate', () => body.data.rates.forEach(r => {",
                  "  pm.expect(r).to.have.property('rate');",
                  "  pm.expect(r).to.have.property('inverse_rate');",
                  "}));",
                  "console.log(`${body.data.source.toUpperCase()} ${body.data.rate_date}: ${body.data.rates.length} rate(s)`);"
                ]
              }
            }
          ],
          "response": [
            {
              "name": "ECB 2026-09-18 (USD, GBP)",
              "originalRequest": {
                "method": "GET",
                "header": [
                  {
                    "key": "X-API-Key",
                    "value": "{{apiKey}}"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/public/rates/ecb/2026-09-18?currencies=USD,GBP",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "public",
                    "rates",
                    "ecb",
                    "2026-09-18"
                  ],
                  "query": [
                    {
                      "key": "currencies",
                      "value": "USD,GBP"
                    }
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"data\": {\n    \"source\": \"ecb\",\n    \"source_name\": \"European Central Bank\",\n    \"base_currency\": \"EUR\",\n    \"requested_date\": \"2026-09-18\",\n    \"rate_date\": \"2026-09-18\",\n    \"rates\": [\n      {\n        \"currency\": \"GBP\",\n        \"rate_type\": \"reference\",\n        \"rate\": \"1.16441546344\",\n        \"inverse_rate\": \"0.8588\"\n      },\n      {\n        \"currency\": \"USD\",\n        \"rate_type\": \"reference\",\n        \"rate\": \"0.87260034904\",\n        \"inverse_rate\": \"1.146\"\n      }\n    ]\n  },\n  \"message\": \"2 rate(s)\",\n  \"status\": \"success\"\n}"
            }
          ]
        },
        {
          "name": "Latest rates",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/public/rates/{{source}}/latest",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "public",
                "rates",
                "{{source}}",
                "latest"
              ]
            },
            "description": "Most recent published rates of the source."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('Rates returned', () => pm.expect(body.data.rates).to.be.an('array').that.is.not.empty);",
                  "pm.test('Each rate has rate and inverse_rate', () => body.data.rates.forEach(r => {",
                  "  pm.expect(r).to.have.property('rate');",
                  "  pm.expect(r).to.have.property('inverse_rate');",
                  "}));",
                  "console.log(`${body.data.source.toUpperCase()} ${body.data.rate_date}: ${body.data.rates.length} rate(s)`);"
                ]
              }
            }
          ]
        },
        {
          "name": "Selected currencies",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/public/rates/ecb/{{rateDate}}?currencies=USD,GBP,CHF",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "public",
                "rates",
                "ecb",
                "{{rateDate}}"
              ],
              "query": [
                {
                  "key": "currencies",
                  "value": "USD,GBP,CHF",
                  "description": "Comma-separated ISO codes"
                }
              ]
            },
            "description": "Only the listed currencies."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('Rates returned', () => pm.expect(body.data.rates).to.be.an('array').that.is.not.empty);",
                  "pm.test('Each rate has rate and inverse_rate', () => body.data.rates.forEach(r => {",
                  "  pm.expect(r).to.have.property('rate');",
                  "  pm.expect(r).to.have.property('inverse_rate');",
                  "}));",
                  "console.log(`${body.data.source.toUpperCase()} ${body.data.rate_date}: ${body.data.rates.length} rate(s)`);"
                ]
              }
            }
          ]
        },
        {
          "name": "TCMB forex buying (USD, EUR)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/public/rates/tcmb/{{rateDate}}?currencies=USD,EUR&rate_type=forex_buying",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "public",
                "rates",
                "tcmb",
                "{{rateDate}}"
              ],
              "query": [
                {
                  "key": "currencies",
                  "value": "USD,EUR"
                },
                {
                  "key": "rate_type",
                  "value": "forex_buying"
                }
              ]
            },
            "description": "Single rate type. TCMB types: forex_buying, forex_selling, banknote_buying, banknote_selling."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('Rates returned', () => pm.expect(body.data.rates).to.be.an('array').that.is.not.empty);",
                  "pm.test('Each rate has rate and inverse_rate', () => body.data.rates.forEach(r => {",
                  "  pm.expect(r).to.have.property('rate');",
                  "  pm.expect(r).to.have.property('inverse_rate');",
                  "}));",
                  "console.log(`${body.data.source.toUpperCase()} ${body.data.rate_date}: ${body.data.rates.length} rate(s)`);"
                ]
              }
            }
          ]
        },
        {
          "name": "Weekend with fallback",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/public/rates/ecb/2026-09-13?fallback=true&currencies=USD",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "public",
                "rates",
                "ecb",
                "2026-09-13"
              ],
              "query": [
                {
                  "key": "fallback",
                  "value": "true"
                },
                {
                  "key": "currencies",
                  "value": "USD"
                }
              ]
            },
            "description": "Sunday: returns the most recent earlier publication (up to 10 days back). `rate_date` shows the date returned."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('Rates returned', () => pm.expect(body.data.rates).to.be.an('array').that.is.not.empty);",
                  "pm.test('Each rate has rate and inverse_rate', () => body.data.rates.forEach(r => {",
                  "  pm.expect(r).to.have.property('rate');",
                  "  pm.expect(r).to.have.property('inverse_rate');",
                  "}));",
                  "console.log(`${body.data.source.toUpperCase()} ${body.data.rate_date}: ${body.data.rates.length} rate(s)`);",
                  "pm.test('Earlier date returned', () => pm.expect(pm.response.json().data.rate_date < '2026-09-13').to.be.true);"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "2. Latest rates per source",
      "description": "One request per source. Sources outside your plan return 403.",
      "item": [
        {
          "name": "TCMB",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/public/rates/tcmb/latest",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "public",
                "rates",
                "tcmb",
                "latest"
              ]
            },
            "description": "Central Bank of the Republic of Turkey — base TRY, 4 rate types"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('Rates returned', () => pm.expect(body.data.rates).to.be.an('array').that.is.not.empty);",
                  "pm.test('Each rate has rate and inverse_rate', () => body.data.rates.forEach(r => {",
                  "  pm.expect(r).to.have.property('rate');",
                  "  pm.expect(r).to.have.property('inverse_rate');",
                  "}));",
                  "console.log(`${body.data.source.toUpperCase()} ${body.data.rate_date}: ${body.data.rates.length} rate(s)`);"
                ]
              }
            }
          ]
        },
        {
          "name": "ECB",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/public/rates/ecb/latest",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "public",
                "rates",
                "ecb",
                "latest"
              ]
            },
            "description": "European Central Bank — base EUR"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('Rates returned', () => pm.expect(body.data.rates).to.be.an('array').that.is.not.empty);",
                  "pm.test('Each rate has rate and inverse_rate', () => body.data.rates.forEach(r => {",
                  "  pm.expect(r).to.have.property('rate');",
                  "  pm.expect(r).to.have.property('inverse_rate');",
                  "}));",
                  "console.log(`${body.data.source.toUpperCase()} ${body.data.rate_date}: ${body.data.rates.length} rate(s)`);"
                ]
              }
            }
          ]
        },
        {
          "name": "CNB",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/public/rates/cnb/latest",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "public",
                "rates",
                "cnb",
                "latest"
              ]
            },
            "description": "Czech National Bank — base CZK"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('Rates returned', () => pm.expect(body.data.rates).to.be.an('array').that.is.not.empty);",
                  "pm.test('Each rate has rate and inverse_rate', () => body.data.rates.forEach(r => {",
                  "  pm.expect(r).to.have.property('rate');",
                  "  pm.expect(r).to.have.property('inverse_rate');",
                  "}));",
                  "console.log(`${body.data.source.toUpperCase()} ${body.data.rate_date}: ${body.data.rates.length} rate(s)`);"
                ]
              }
            }
          ]
        },
        {
          "name": "MNB",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/public/rates/mnb/latest",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "public",
                "rates",
                "mnb",
                "latest"
              ]
            },
            "description": "Magyar Nemzeti Bank (Hungary) — base HUF"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('Rates returned', () => pm.expect(body.data.rates).to.be.an('array').that.is.not.empty);",
                  "pm.test('Each rate has rate and inverse_rate', () => body.data.rates.forEach(r => {",
                  "  pm.expect(r).to.have.property('rate');",
                  "  pm.expect(r).to.have.property('inverse_rate');",
                  "}));",
                  "console.log(`${body.data.source.toUpperCase()} ${body.data.rate_date}: ${body.data.rates.length} rate(s)`);"
                ]
              }
            }
          ]
        },
        {
          "name": "BOI",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/public/rates/boi/latest",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "public",
                "rates",
                "boi",
                "latest"
              ]
            },
            "description": "Bank of Israel — base ILS"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('Rates returned', () => pm.expect(body.data.rates).to.be.an('array').that.is.not.empty);",
                  "pm.test('Each rate has rate and inverse_rate', () => body.data.rates.forEach(r => {",
                  "  pm.expect(r).to.have.property('rate');",
                  "  pm.expect(r).to.have.property('inverse_rate');",
                  "}));",
                  "console.log(`${body.data.source.toUpperCase()} ${body.data.rate_date}: ${body.data.rates.length} rate(s)`);"
                ]
              }
            }
          ]
        },
        {
          "name": "BNM",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/public/rates/bnm/latest",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "public",
                "rates",
                "bnm",
                "latest"
              ]
            },
            "description": "Bank Negara Malaysia — base MYR, buying/selling/reference"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('Rates returned', () => pm.expect(body.data.rates).to.be.an('array').that.is.not.empty);",
                  "pm.test('Each rate has rate and inverse_rate', () => body.data.rates.forEach(r => {",
                  "  pm.expect(r).to.have.property('rate');",
                  "  pm.expect(r).to.have.property('inverse_rate');",
                  "}));",
                  "console.log(`${body.data.source.toUpperCase()} ${body.data.rate_date}: ${body.data.rates.length} rate(s)`);"
                ]
              }
            }
          ]
        },
        {
          "name": "CFETS",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/public/rates/cfets/latest",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "public",
                "rates",
                "cfets",
                "latest"
              ]
            },
            "description": "CFETS / ChinaMoney central parity — base CNY"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('Rates returned', () => pm.expect(body.data.rates).to.be.an('array').that.is.not.empty);",
                  "pm.test('Each rate has rate and inverse_rate', () => body.data.rates.forEach(r => {",
                  "  pm.expect(r).to.have.property('rate');",
                  "  pm.expect(r).to.have.property('inverse_rate');",
                  "}));",
                  "console.log(`${body.data.source.toUpperCase()} ${body.data.rate_date}: ${body.data.rates.length} rate(s)`);"
                ]
              }
            }
          ]
        },
        {
          "name": "RIKSBANK",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/public/rates/riksbank/latest",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "public",
                "rates",
                "riksbank",
                "latest"
              ]
            },
            "description": "Sveriges Riksbank (Sweden) — base SEK"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('Rates returned', () => pm.expect(body.data.rates).to.be.an('array').that.is.not.empty);",
                  "pm.test('Each rate has rate and inverse_rate', () => body.data.rates.forEach(r => {",
                  "  pm.expect(r).to.have.property('rate');",
                  "  pm.expect(r).to.have.property('inverse_rate');",
                  "}));",
                  "console.log(`${body.data.source.toUpperCase()} ${body.data.rate_date}: ${body.data.rates.length} rate(s)`);"
                ]
              }
            }
          ]
        },
        {
          "name": "BANXICO",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/public/rates/banxico/latest",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "public",
                "rates",
                "banxico",
                "latest"
              ]
            },
            "description": "Banco de Mexico — base MXN"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('Rates returned', () => pm.expect(body.data.rates).to.be.an('array').that.is.not.empty);",
                  "pm.test('Each rate has rate and inverse_rate', () => body.data.rates.forEach(r => {",
                  "  pm.expect(r).to.have.property('rate');",
                  "  pm.expect(r).to.have.property('inverse_rate');",
                  "}));",
                  "console.log(`${body.data.source.toUpperCase()} ${body.data.rate_date}: ${body.data.rates.length} rate(s)`);"
                ]
              }
            }
          ]
        },
        {
          "name": "BOT",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/public/rates/bot/latest",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "public",
                "rates",
                "bot",
                "latest"
              ]
            },
            "description": "Bank of Thailand — base THB, buying/selling/reference"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('Rates returned', () => pm.expect(body.data.rates).to.be.an('array').that.is.not.empty);",
                  "pm.test('Each rate has rate and inverse_rate', () => body.data.rates.forEach(r => {",
                  "  pm.expect(r).to.have.property('rate');",
                  "  pm.expect(r).to.have.property('inverse_rate');",
                  "}));",
                  "console.log(`${body.data.source.toUpperCase()} ${body.data.rate_date}: ${body.data.rates.length} rate(s)`);"
                ]
              }
            }
          ]
        },
        {
          "name": "MAS",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/public/rates/mas/latest",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "public",
                "rates",
                "mas",
                "latest"
              ]
            },
            "description": "Monetary Authority of Singapore — base SGD"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('Rates returned', () => pm.expect(body.data.rates).to.be.an('array').that.is.not.empty);",
                  "pm.test('Each rate has rate and inverse_rate', () => body.data.rates.forEach(r => {",
                  "  pm.expect(r).to.have.property('rate');",
                  "  pm.expect(r).to.have.property('inverse_rate');",
                  "}));",
                  "console.log(`${body.data.source.toUpperCase()} ${body.data.rate_date}: ${body.data.rates.length} rate(s)`);"
                ]
              }
            }
          ]
        },
        {
          "name": "HMRC",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/public/rates/hmrc/latest",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "public",
                "rates",
                "hmrc",
                "latest"
              ]
            },
            "description": "HM Revenue & Customs (UK, monthly) — base GBP"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('Rates returned', () => pm.expect(body.data.rates).to.be.an('array').that.is.not.empty);",
                  "pm.test('Each rate has rate and inverse_rate', () => body.data.rates.forEach(r => {",
                  "  pm.expect(r).to.have.property('rate');",
                  "  pm.expect(r).to.have.property('inverse_rate');",
                  "}));",
                  "console.log(`${body.data.source.toUpperCase()} ${body.data.rate_date}: ${body.data.rates.length} rate(s)`);"
                ]
              }
            }
          ]
        },
        {
          "name": "BMF",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/public/rates/bmf/latest",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "public",
                "rates",
                "bmf",
                "latest"
              ]
            },
            "description": "German Federal Ministry of Finance (monthly, ECB-based) — base EUR"
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 200', () => pm.response.to.have.status(200));",
                  "const body = pm.response.json();",
                  "pm.test('Rates returned', () => pm.expect(body.data.rates).to.be.an('array').that.is.not.empty);",
                  "pm.test('Each rate has rate and inverse_rate', () => body.data.rates.forEach(r => {",
                  "  pm.expect(r).to.have.property('rate');",
                  "  pm.expect(r).to.have.property('inverse_rate');",
                  "}));",
                  "console.log(`${body.data.source.toUpperCase()} ${body.data.rate_date}: ${body.data.rates.length} rate(s)`);"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "3. Error examples",
      "item": [
        {
          "name": "Missing API key (401)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/public/rates/ecb/latest",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "public",
                "rates",
                "ecb",
                "latest"
              ]
            },
            "description": "No X-API-Key header.",
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 401', () => pm.response.to.have.status(401));"
                ]
              }
            }
          ]
        },
        {
          "name": "Unknown source (404)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/public/rates/xyz/latest",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "public",
                "rates",
                "xyz",
                "latest"
              ]
            },
            "description": ""
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 404', () => pm.response.to.have.status(404));"
                ]
              }
            }
          ]
        },
        {
          "name": "Invalid date format (400)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/public/rates/ecb/18-09-2026",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "public",
                "rates",
                "ecb",
                "18-09-2026"
              ]
            },
            "description": "Dates must be YYYY-MM-DD."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 400', () => pm.response.to.have.status(400));"
                ]
              }
            }
          ]
        },
        {
          "name": "Future date (400)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/public/rates/ecb/2099-01-01",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "public",
                "rates",
                "ecb",
                "2099-01-01"
              ]
            },
            "description": ""
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 400', () => pm.response.to.have.status(400));"
                ]
              }
            }
          ]
        },
        {
          "name": "Weekend without fallback (404)",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/public/rates/ecb/2026-09-13",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "public",
                "rates",
                "ecb",
                "2026-09-13"
              ]
            },
            "description": "Add fallback=true to get the previous publication."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 404', () => pm.response.to.have.status(404));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "4. Portal – API key management",
      "description": "Optional. Uses your portal login (JWT, valid 15 minutes) instead of the API key. Set `email` and `password`, run **Login**, then **Create API key**. The new key is stored in `apiKey` automatically. Create, rotate and IP changes require your portal password and send an email notification to the account owner. Note: **Revoke API key** revokes the key in `apiKeyId` (the one created or rotated here); after it, calls with that key return 401.",
      "auth": {
        "type": "bearer",
        "bearer": [
          {
            "key": "token",
            "value": "{{accessToken}}",
            "type": "string"
          }
        ]
      },
      "item": [
        {
          "name": "Login",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/auth/login",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "auth",
                "login"
              ]
            },
            "description": "Portal login. Stores the access token in `accessToken`.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"{{email}}\",\n  \"password\": \"{{password}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "auth": {
              "type": "noauth"
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 200', () => pm.response.to.have.status(200));",
                  "pm.collectionVariables.set('accessToken', pm.response.json().data.access_token);"
                ]
              }
            }
          ]
        },
        {
          "name": "Rates API access",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api-keys/access",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api-keys",
                "access"
              ]
            },
            "description": "Whether your plan includes the Rates API (Enterprise)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 200', () => pm.response.to.have.status(200));",
                  "pm.test('Enabled for this plan', () => pm.expect(pm.response.json().data.enabled).to.be.true);"
                ]
              }
            }
          ]
        },
        {
          "name": "Create API key",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api-keys",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api-keys"
              ]
            },
            "description": "Creates a key. The plaintext key is returned only once and saved to `apiKey`.\n\n- `password`: your portal password (confirmation)\n- `expires_in_days`: 30, 90, 180, 365 or 730\n- `allowed_ips` (required): 1-5 public IPs or ranges (/24 or narrower) of the servers that will call the API. Set the `serverIp` variable to the public IP of the machine running Postman to test from here.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Postman\",\n  \"password\": \"{{password}}\",\n  \"expires_in_days\": 365,\n  \"allowed_ips\": [\n    \"{{serverIp}}\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 201', () => pm.response.to.have.status(201));",
                  "const d = pm.response.json().data;",
                  "pm.collectionVariables.set('apiKey', d.api_key);",
                  "pm.collectionVariables.set('apiKeyId', d.id);",
                  "console.log('API key stored in collection variable apiKey (prefix ' + d.key_prefix + ')');"
                ]
              }
            }
          ]
        },
        {
          "name": "List API keys",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api-keys",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api-keys"
              ]
            },
            "description": "Active keys (prefix only, never the full key)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 200', () => pm.response.to.have.status(200));"
                ]
              }
            }
          ]
        },
        {
          "name": "Update allowed IPs",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 200', () => pm.response.to.have.status(200));"
                ]
              }
            }
          ],
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api-keys/{{apiKeyId}}/allowed-ips",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api-keys",
                "{{apiKeyId}}",
                "allowed-ips"
              ]
            },
            "description": "Replaces the IP allowlist of the key in `apiKeyId` (1-5 public IPs or ranges, /24 or narrower). Requires your portal password.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"allowed_ips\": [\n    \"{{serverIp}}\"\n  ],\n  \"password\": \"{{password}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Rotate API key",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 201', () => pm.response.to.have.status(201));",
                  "const d = pm.response.json().data;",
                  "pm.collectionVariables.set('apiKey', d.api_key);",
                  "pm.collectionVariables.set('apiKeyId', d.id);",
                  "console.log('New key stored in apiKey (prefix ' + d.key_prefix + '); previous key expires in 7 days');"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api-keys/{{apiKeyId}}/rotate",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api-keys",
                "{{apiKeyId}}",
                "rotate"
              ]
            },
            "description": "Issues a new key with the same name and IP allowlist and stores it in `apiKey`. The previous key keeps working for 7 days.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"password\": \"{{password}}\",\n  \"expires_in_days\": 365\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          }
        },
        {
          "name": "Revoke API key",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api-keys/{{apiKeyId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "api-keys",
                "{{apiKeyId}}"
              ]
            },
            "description": "Revokes the key in `apiKeyId`; calls using it are rejected immediately."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Status is 200', () => pm.response.to.have.status(200));"
                ]
              }
            }
          ]
        }
      ]
    }
  ]
}