Use an Apps Script community connector to show USD to EUR and JPY exchange rates in Looker Studio. The connector requests rates from exchangerate.dev and returns a row for each currency, with a rate and an update value. You supply an API key and choose the currencies.
Create the Apps Script project
Create an Apps Script project for the community connector and get an API key from signup. Keep the key ready for the authorization step in Looker Studio. A free key has 5,000 requests a month and 10 requests a minute; its rates are 15 minutes late.
The project uses getAuthType, getConfig, getSchema and getData. These functions declare key authentication, collect the currency inputs, define the fields and return the requested rows. The authentication functions store the key in user properties. The API request reads that stored value and sends it in the Authorization header as a Bearer token.
Add the connector code and manifest
Paste the connector code into Code.gs. In Apps Script project settings, show the appsscript.json manifest file, then paste the manifest snippet into appsscript.json. Both snippets belong in the same project.
The manifest has a dataStudio block with the connector name, logo URL, company, support URL and description. The script contains the authentication and data functions. Its schema declares currency and updated as dimensions, and rate as a metric. The updated dimension uses the year, month, day and hour from the API observation value.
var cc = DataStudioApp.createCommunityConnector();function getAuthType() { return cc.newAuthTypeResponse().setAuthType(cc.AuthType.KEY).setHelpUrl('https://exchangerate.dev/signup').build();}function isAuthValid() { return !!PropertiesService.getUserProperties().getProperty('dscc.key');}function setCredentials(request) { PropertiesService.getUserProperties().setProperty('dscc.key', request.key); return { errorCode: 'NONE' };}function resetAuth() { PropertiesService.getUserProperties().deleteProperty('dscc.key');}function isAdminUser() { return false;}function getConfig() { var config = cc.getConfig(); config.newTextInput().setId('base').setName('Base currency').setPlaceholder('USD'); config.newTextInput().setId('symbols').setName('Currencies').setHelpText('Comma-separated codes. Empty means EUR,JPY.').setPlaceholder('EUR,JPY'); return config.build();}function getSchema(request) { var fields = cc.getFields(); fields.newDimension().setId('currency').setName('Currency').setType(cc.FieldType.TEXT); fields.newMetric().setId('rate').setName('Rate').setType(cc.FieldType.NUMBER); fields.newDimension().setId('updated').setName('Updated').setType(cc.FieldType.YEAR_MONTH_DAY_HOUR); return { schema: fields.build() };}function getData(request) { var key = PropertiesService.getUserProperties().getProperty('dscc.key'); var p = request.configParams || {}; var base = p.base || 'USD'; var symbols = p.symbols || 'EUR,JPY'; var url = 'https://api.exchangerate.dev/v1/latest/' + encodeURIComponent(base) + '?symbols=' + encodeURIComponent(symbols); // Keyless and free keys allow 10 requests a minute, so a report that refreshes often reads the cached answer for 5 minutes. var cache = CacheService.getUserCache(); var text = cache.get(url); if (!text) { text = UrlFetchApp.fetch(url, { headers: { Authorization: 'Bearer ' + key } }).getContentText(); cache.put(url, text, 300); } var body = JSON.parse(text); var updated = body.data_updated_at.replace(/[-:TZ]/g, '').slice(0, 10); var requested = cc.getFields().forIds(request.fields.map(function (f) { return f.name; })); var rows = Object.keys(body.rates).map(function (code) { var values = { currency: code, rate: body.rates[code], updated: updated }; return { values: request.fields.map(function (f) { return values[f.name]; }) }; }); return { schema: requested.build(), rows: rows };}{ "timeZone": "UTC", "exceptionLogging": "STACKDRIVER", "runtimeVersion": "V8", "dataStudio": { "name": "Exchange rates", "logoUrl": "https://exchangerate.dev/icon.png", "company": "exchangerate.dev", "companyUrl": "https://exchangerate.dev", "addonUrl": "https://exchangerate.dev/docs/integrations/looker-studio", "supportUrl": "https://exchangerate.dev/about", "privacyPolicyUrl": "https://exchangerate.dev/privacy", "termsOfServiceUrl": "https://exchangerate.dev/terms", "description": "Latest exchange rates for a base currency and a list of currencies." }}Configure the currencies
Use USD for the base currency and EUR,JPY for the comma-separated symbols list. Empty inputs use those defaults. The code also handles a missing request.configParams object before it reads either input.
With these settings, getData calls GET https://api.exchangerate.dev/v1/latest/USD?symbols=EUR,JPY with the stored API key in the Bearer header. The base determines how to read every returned rate: each value is an amount of the target currency per USD.
The request URL contains both the base and symbols. The connector uses that URL as its user-cache key, so different currency configurations have separate cached responses.
Test the connector
Deploy the Apps Script project as a test deployment, then add it as a data source in Looker Studio. Authorize the connector with your exchangerate.dev API key. Enter USD as the base and EUR,JPY as the currencies.
Check that the table has a row for EUR and a row for JPY. Each row should have a rate and an update value. The rate belongs to the currency on that row; the base comes from your configuration.
If you leave the currency inputs empty, the connector uses the same defaults. The key you paste during authorization is stored in user properties, separately from the base and symbols inputs.
Read the returned rows
The result contains a schema and rows whose values follow the requested field order. In the sample, that order is currency, rate and updated. The connector returns the fields Looker Studio requests, so getData builds both the schema and row values from request.fields.
Read the EUR rate as EUR per USD and the JPY rate as JPY per USD. The updated value comes from data_updated_at, the oldest contributing observation in the API response. The code removes the timestamp separators and keeps the date and hour for the updated dimension. It uses that response-level update value for each currency row.
{ "schema": [ { "name": "currency" }, { "name": "rate" }, { "name": "updated" } ], "rows": [ { "values": [ "EUR", 0.8934, "2026102318" ] }, { "values": [ "JPY", 156.885, "2026102318" ] } ]}Account for caching and rate timing
The connector caches each response per user for 5 minutes with CacheService user cache. Requests for the same URL during that period read the cached response. This limits API calls when a report refreshes often, since free keys have a limit of 10 requests a minute.
Free-key rates are 15 minutes late; paid plans are live. The connector's cache can retain a response during its cache period on either plan.
Rates are indicative mid-market figures, not for settlement or trading. Banks, cards and transfer services apply their own spread and fees.