Install our app for a better experience!

Recipe: Scores into Google Sheets

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

  1. Create a new Google Sheet (sheets.new). Name it e.g. PrepareBuddy results.
  2. Menu Extensions → Apps Script. A code editor opens in a new tab.
  3. Delete everything in the editor (the empty myFunction).

Step 2 — Store your API key safely

Do not paste the key into the code.

  1. In the Apps Script editor, click the gear icon (Project Settings) on the left.
  2. Scroll to Script properties → Add script property.
  3. Property: PREPAREBUDDY_API_KEY Value: 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

  1. In the toolbar, choose syncResults in the function list and click Run.
  2. 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.)
  3. Go back to the sheet: a Results tab is filled in.
  4. Optional: run syncDailyTrend for the Daily trend tab.
  5. 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.