Recipe: Scores into Google Sheets (no server needed)
A Google Sheet that fills itself with every student result, updates re-marked scores in place, and refreshes every night. You copy one script — no programming knowledge needed. About 10 minutes.
You need: a Google account, and a read-only PrepareBuddy API key (API Quick Start → Step 1).
Step 1 — Make the sheet and open the script editor
- Create a new Google Sheet (sheets.new). Name it e.g. PrepareBuddy results.
- Menu Extensions → Apps Script. A code editor opens in a new tab.
- Delete everything in the editor (the empty
myFunction).
Step 2 — Store your API key safely
Do not paste the key into the code.
- In the Apps Script editor, click the gear icon (Project Settings) on the left.
- Scroll to Script properties → Add script property.
- Property:
PREPAREBUDDY_API_KEYValue: your key. Click Save script properties.
Step 3 — Paste the script
Click the < > Editor icon, paste all of this, and click the Save icon:
// PrepareBuddy → Google Sheets. Pulls every final result; re-marked results update their row.
var BASE = 'https://www.preparebuddy.com/api/v1'; // your PrepareBuddy address (keep the www.)
var RESULT_HEADERS = ['Result ID', 'Email', 'First name', 'Last name', 'Kind', 'Title', 'Test type',
'Score', 'Out of', 'Percentage', 'Completed', 'Released', 'Result link'];
function syncResults() {
var props = PropertiesService.getScriptProperties();
var key = props.getProperty('PREPAREBUDDY_API_KEY');
if (!key) throw new Error('Add PREPAREBUDDY_API_KEY under Project Settings > Script properties.');
var sheet = sheet_('Results', RESULT_HEADERS);
var rowById = rowsById_(sheet);
var cursor = props.getProperty('RESULTS_CURSOR') || '';
var tz = encodeURIComponent(Session.getScriptTimeZone());
for (var page = 0; page < 50; page++) { // up to 25,000 results per run
var url = BASE + '/results/?limit=500&tz=' + tz + (cursor ? '&cursor=' + encodeURIComponent(cursor) : '');
var body = get_(url, key);
body.results.forEach(function (r) {
var row = [r.id, r.student.email, r.student.first_name, r.student.last_name, r.kind, r.item.title,
r.item.type_label || r.item.type || '', r.score, r.max_score, r.percentage,
r.completed_at || '', r.released_at || '', r.result_url];
if (rowById[r.id]) {
sheet.getRange(rowById[r.id], 1, 1, row.length).setValues([row]); // re-marked: update in place
} else {
sheet.appendRow(row);
rowById[r.id] = sheet.getLastRow();
}
});
if (body.next_cursor) {
cursor = body.next_cursor;
props.setProperty('RESULTS_CURSOR', cursor); // next run continues from here
}
if (!body.has_more) break;
}
}
// Optional: a "Daily trend" tab — one row per student per day per test type, last 30 days.
function syncDailyTrend() {
var key = PropertiesService.getScriptProperties().getProperty('PREPAREBUDDY_API_KEY');
var headers = ['Date', 'Email', 'First name', 'Kind', 'Test type', 'Attempts',
'Average %', 'Best %', 'Average score', 'Best score', 'Out of'];
var sheet = sheet_('Daily trend', headers);
var body = get_(BASE + '/progress/daily/?tz=' + encodeURIComponent(Session.getScriptTimeZone()), key);
var rows = body.days.map(function (d) {
return [d.date, d.student.email, d.student.first_name, d.kind, d.type_label || d.type, d.attempts,
d.average_percentage, d.best_percentage, d.average_score, d.best_score, d.max_score];
});
if (sheet.getLastRow() > 1) sheet.getRange(2, 1, sheet.getLastRow() - 1, headers.length).clearContent();
if (rows.length) sheet.getRange(2, 1, rows.length, headers.length).setValues(rows);
}
// Run once: refresh both tabs every night at about 2 am.
function installNightlyRefresh() {
ScriptApp.getProjectTriggers().forEach(function (t) { ScriptApp.deleteTrigger(t); });
ScriptApp.newTrigger('syncResults').timeBased().everyDays(1).atHour(2).create();
ScriptApp.newTrigger('syncDailyTrend').timeBased().everyDays(1).atHour(3).create();
}
// Run if you want to start again from the very first result.
function startOver() {
PropertiesService.getScriptProperties().deleteProperty('RESULTS_CURSOR');
}
function get_(url, key) {
var res = UrlFetchApp.fetch(url, {headers: {'X-API-Key': key}, muteHttpExceptions: true});
if (res.getResponseCode() !== 200) {
throw new Error('PrepareBuddy answered ' + res.getResponseCode() + ': ' + res.getContentText());
}
return JSON.parse(res.getContentText());
}
function sheet_(name, headers) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName(name) || ss.insertSheet(name);
if (sheet.getLastRow() === 0) sheet.appendRow(headers);
return sheet;
}
function rowsById_(sheet) {
var map = {};
var last = sheet.getLastRow();
if (last < 2) return map;
sheet.getRange(2, 1, last - 1, 1).getValues().forEach(function (v, i) { map[v[0]] = i + 2; });
return map;
}
Step 4 — Run it
- In the toolbar, choose syncResults in the function list and click Run.
- The first time, Google asks for permission: Review permissions → choose your account → Advanced → Go to (your project) → Allow. (It needs to reach PrepareBuddy and edit this sheet.)
- Go back to the sheet: a Results tab is filled in.
- Optional: run syncDailyTrend for the Daily trend tab.
- Run installNightlyRefresh once — from now on both tabs refresh themselves every night.
How it behaves
- The first run fetches everything; later runs fetch only what changed since the last run (the script stores a bookmark called a cursor).
- A re-marked result (same Result ID) overwrites its row; nothing is duplicated.
- Times are in the sheet's time zone (File → Settings → Time zone).
- A run that is interrupted continues where it stopped next time.
Want other data too?
Change the address and the columns in syncDailyTrend's pattern:
| Tab | Address | Useful fields |
|---|---|---|
| Students | /students/?limit=500 (use next_cursor like syncResults) |
email, enrollments[0].course_name, enrollments[0].end_date, enrollments[0].has_access |
| Assignments | /assignments/?status=open |
student.email, item.title, status, due_date, overdue |
| Activity | /activity/daily/ |
date, student.email, active_minutes, tests_completed |
Field details: Roster, Assignments & Activity API.
Why not a webhook straight into a Google Sheet? Google Apps Script web apps answer every incoming request with a redirect, and PrepareBuddy (correctly) treats a redirect as a failed delivery. For live updates into a sheet, use Zapier, Pabbly or Make between the two.
Troubleshooting
| Message | Fix |
|---|---|
Add PREPAREBUDDY_API_KEY… |
Step 2 — the property name must be exactly PREPAREBUDDY_API_KEY. |
PrepareBuddy answered 401 |
The key is wrong or revoked. Create a new one and update the property. |
PrepareBuddy answered 400 … cursor |
Run startOver, then syncResults. |
| Nothing new appears | Nothing new became final. Results held for teacher review appear once released. |
