On this page
A spreadsheet is often all a local paper, a town website, a film society or a student media needs: the programme of the cinema down the road, updated by itself, that anyone on the team can filter, sort or publish. This tutorial puts a cinema's showtimes for the coming week in a Google Sheet with about 80 lines of Apps Script, pasted once. No server, no deployment, and one request a day, which the free plan covers many times over.
Date Time Film Runtime Version Format Booking
2026-11-06 11:50 Sourires d'une nuit d'été 1 h 49 VOST 2D https://www.cinema-lechampo.com/…
2026-11-06 11:55 Le Septième Sceau 1 h 36 VOST 2D https://www.cinema-lechampo.com/…
2026-11-06 13:45 Affreux, sales et méchants 1 h 55 VOST 2D https://www.cinema-lechampo.com/…What you need
- A Google account and a new Google Sheet.
- An API Cinéma key: create a free account, confirm your e-mail address, then create a key from the dashboard. The free plan gives 100 requests a week; this sheet uses one a day.
- The
slugof the cinema. Search it by name:
curl "https://api.api-cinema.com/v1/cinemas?q=champo" -H "Authorization: Bearer ac_live_…"The answer gives "slug": "le-champo-paris-5e-arrondissement". Any cinema of the API, in France, the United States or Australia, works the same way.
The script
In the sheet, open Extensions › Apps Script, replace the content of Code.gs with the script below and save it (the disk icon, or Ctrl+S). Then reload the sheet: a Cinéma menu appears next to Help.
// A Google Sheet that fills itself with a cinema's showtimes, from API Cinéma.
// Menu "Cinéma": set the key once, update now, or every morning.
const API = 'https://api.api-cinema.com/v1'
const HEADER = ['Date', 'Time', 'Film', 'Runtime', 'Version', 'Format', 'Booking']
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('Cinéma')
.addItem('Update showtimes', 'updateShowtimes')
.addItem('Update every morning', 'installDailyTrigger')
.addSeparator()
.addItem('Set API key…', 'setApiKey')
.addToUi()
}
/** The key lives in the script properties: never in a cell, never in the code. */
function setApiKey() {
const ui = SpreadsheetApp.getUi()
const answer = ui.prompt('API Cinéma key', 'Paste your key (ac_live_…).', ui.ButtonSet.OK_CANCEL)
if (answer.getSelectedButton() !== ui.Button.OK)
return
PropertiesService.getScriptProperties().setProperty('API_CINEMA_KEY', answer.getResponseText().trim())
}
function updateShowtimes() {
const key = PropertiesService.getScriptProperties().getProperty('API_CINEMA_KEY')
if (!key)
throw new Error('No API key yet: Cinéma › Set API key…')
const ss = SpreadsheetApp.getActiveSpreadsheet()
const config = configSheet(ss)
const slug = String(config.getRange('B1').getValue()).trim()
const days = Math.max(1, Math.min(14, Number(config.getRange('B2').getValue()) || 7))
if (!slug)
throw new Error('Put the cinema slug in Config!B1')
const rows = fetchShowtimes(key, slug, days).map(s => [
s.startsAt.slice(0, 10), // the cinema's local date and time, as the API gives them
s.startsAt.slice(11, 16),
s.movie.title,
s.movie.durationMin ? `${Math.floor(s.movie.durationMin / 60)} h ${String(s.movie.durationMin % 60).padStart(2, '0')}` : '',
s.version,
s.format,
s.bookingUrl || '',
])
const sheet = ss.getSheetByName('Showtimes') || ss.insertSheet('Showtimes')
sheet.clearContents()
sheet.getRange(1, 1, 1, HEADER.length).setValues([HEADER]).setFontWeight('bold')
if (rows.length)
sheet.getRange(2, 1, rows.length, HEADER.length).setValues(rows)
sheet.setFrozenRows(1)
sheet.autoResizeColumns(1, HEADER.length)
config.getRange('B3').setValue(new Date())
}
/** Every showtime of the cinema from now to `days` days ahead, page by page. */
function fetchShowtimes(key, slug, days) {
const to = new Date()
to.setDate(to.getDate() + days - 1) // `to` is a whole day: 7 days = today and the 6 after
const params = { to: Utilities.formatDate(to, Session.getScriptTimeZone(), 'yyyy-MM-dd'), include: 'movie', limit: 1000 }
const all = []
let cursor
do {
const query = Object.entries({ ...params, cursor })
.filter(([, v]) => v !== undefined)
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
.join('&')
const res = UrlFetchApp.fetch(`${API}/cinemas/${encodeURIComponent(slug)}/showtimes?${query}`, {
headers: { Authorization: `Bearer ${key}` },
muteHttpExceptions: true,
})
if (res.getResponseCode() !== 200) {
throw new Error(`API Cinéma answered ${res.getResponseCode()}: ${res.getContentText().slice(0, 200)}`)
}
const body = JSON.parse(res.getContentText())
all.push(...body.data)
cursor = body.meta && body.meta.nextCursor
} while (cursor)
return all
}
/** The Config sheet, created with defaults the first time. */
function configSheet(ss) {
let sheet = ss.getSheetByName('Config')
if (!sheet) {
sheet = ss.insertSheet('Config')
sheet.getRange('A1:B3').setValues([
['Cinema slug', 'le-champo-paris-5e-arrondissement'],
['Days', 7],
['Last update', ''],
])
sheet.autoResizeColumns(1, 2)
}
return sheet
}
/** One update a day, around 6 in the morning, in the spreadsheet's time zone. */
function installDailyTrigger() {
ScriptApp.getProjectTriggers()
.filter(t => t.getHandlerFunction() === 'updateShowtimes')
.forEach(t => ScriptApp.deleteTrigger(t))
ScriptApp.newTrigger('updateShowtimes').timeBased().everyDays(1).atHour(6).create()
SpreadsheetApp.getUi().alert('The Showtimes sheet will update every morning.')
}Store the key
Cinéma › Set API key… asks for the key and stores it in the script's properties: it never appears in a cell, so the sheet can be shared or published without giving the key away. One thing to know: anyone who can edit the sheet can open the script and read its properties. Share the sheet as Viewer with the public, keep editing to the people you trust, and revoke the key from the dashboard if in doubt.
The first time the script runs, Google asks you to authorize it: Review permissions, choose your account, then, as the script is yours and not reviewed by Google, Advanced › Go to … (unsafe) › Allow. It needs two things: your spreadsheets, and connecting to an external service (the API).
Fill the sheet
Cinéma › Update showtimes creates two sheets. Config holds the cinema slug (B1, Le Champo by default) and the number of days (B2, 7, up to 14); Showtimes gets one row per screening, date, time, film, runtime, version, format and the cinema's booking link, header frozen, columns sized. Change the slug or the days in Config, run it again: the sheet is rewritten.
Times are the cinema's local time, as the API returns them: a cinema in New York shows New York times. Showtimes already started are left out, the list begins now. The runtime and the booking link come with include=movie and the rest of the fields; the documentation lists them all.
Every morning
Cinéma › Update every morning installs a time-based trigger: the sheet updates around 6, in its time zone, with no one opening it. That is one request a day for a week of programme (a multiplex over 14 days can take two or three pages, each a request): a few requests a week on a plan that allows 150. The trigger runs under your account; if you leave the organisation, hand the sheet to someone else and let them install it again.
Publish it
- Share or embed. File › Share › Publish to web gives a public page or an
<iframe>for a site, updated after each run. - Filter for today. In another sheet,
=FILTER(Showtimes!A2:G, Showtimes!A2:A = TEXT(TODAY(), "yyyy-mm-dd"))keeps today's rows;=QUERY(Showtimes!A:G, "select C, count(B) group by C")counts the screenings per film. - Another sheet, another cinema. Run
=IMPORTRANGEfrom a second sheet, or add rows to Config and loop over them inupdateShowtimes: one request per cinema.
Going further
- Original version only. Add
version: 'VOST'(orVO) toparams: the API filters before answering. - Posters.
s.movie.posterUrlis in the answer;=IMAGE(H2)shows it in a cell. - A city instead of a cinema.
GET /v1/showtimes?city=Lyon&include=movie,cinemalists every screening of a city, with the cinema in each row: the same script with a different URL and an extra column.
The same data feeds the Python tutorial (CSV and pandas, for bigger exports) and the website widget when a page needs it live. For a newsletter or a post each evening without code, see the n8n tutorial.