Skip to content
API v1 preview — endpoints and fields may change before general availability.

Google Sheets

How do I pull BetterJobs results into a Google Sheet?

View .md

BetterJobs has no Sheets add-on. You add a small Apps Script function to your sheet. It calls the API with UrlFetchApp and returns a table, so you can type this in a cell:

=BETTERJOBS_SEARCH("Head of RevOps, Head of Revenue Operations", "DE, AT, CH", 7, 25)

The result spills into the cells below and to the right: one header row, then one row per canonical job.

  1. In your sheet, open Extensions → Apps Script.

  2. Open Project Settings (the gear icon). Under Script properties, add a property named BETTERJOBS_API_KEY with your key as the value. The key stays out of the code and out of the sheet.

  3. Back in the editor, replace the contents of Code.gs with the script below and save.

  4. In any cell, type =BETTERJOBS_SEARCH(...) as shown above.

const BETTERJOBS_URL = 'https://api.betterjobs.cc/v1/jobs/search';
const BETTERJOBS_VERSION = '2026-10-01';
/**
* Searches BetterJobs and returns one row per canonical job.
*
* @param {string} titles Comma-separated title keywords, e.g. "Head of RevOps, Head of Revenue Operations".
* @param {string} countries Comma-separated ISO country codes, e.g. "DE, AT, CH".
* @param {number} days Jobs first seen within this many days (1-365).
* @param {number} limit Jobs to return (1-100). Also the credit cap for this cell.
* @return {Array<Array<string|number>>} Header row plus one row per job.
* @customfunction
*/
function BETTERJOBS_SEARCH(titles, countries, days, limit) {
const key = PropertiesService.getScriptProperties().getProperty('BETTERJOBS_API_KEY');
if (!key) throw new Error('Add the BETTERJOBS_API_KEY script property first.');
if (!titles || !countries || !days || !limit) {
throw new Error('Usage: =BETTERJOBS_SEARCH(titles, countries, days, limit)');
}
const list = (value) => String(value).split(',').map((s) => s.trim()).filter((s) => s);
const body = {
filters: {
title_or: list(titles),
country_code_or: list(countries).map((c) => c.toUpperCase()),
posted_within_days: Number(days),
},
waterfall: { strategy: 'cheapest_first', max_credits: Number(limit) },
limit: Number(limit),
};
const res = UrlFetchApp.fetch(BETTERJOBS_URL, {
method: 'post',
contentType: 'application/json',
headers: { Authorization: 'Bearer ' + key, 'BetterJobs-Version': BETTERJOBS_VERSION },
payload: JSON.stringify(body),
muteHttpExceptions: true,
});
const json = JSON.parse(res.getContentText());
if (res.getResponseCode() !== 200) {
throw new Error(json.error.code + ': ' + json.error.message + ' (' + json.error.request_id + ')');
}
const header = ['title', 'company', 'domain', 'country', 'seniority', 'posted_at', 'first_seen_at', 'apply_url', 'sources', 'p_real', 'id'];
const rows = json.data.map((job) => [
job.title,
job.company.name,
job.company.domain ?? '',
job.location.country_code ?? '',
job.seniority ?? '',
job.posted_at ?? '',
job.first_seen_at,
job.apply_url ?? '',
job.sources.map((s) => s.provider).join('|'),
job.p_real,
job.id,
]);
return [header, ...rows];
}

Empty cells mean unknown (null in the API), not “none”. sources lists every provider that saw the job, joined with |, the same format as the sample CSV. Each column is described in the field dictionary.

If the request fails, the cell shows #ERROR!. Hover it to read the error code and message, for example insufficient_credits: .... The request id in brackets is what support needs. Error codes are listed on Errors.

Each cell is a live API call. Read this before you copy the formula down a column.

  • A search with no matches returns only the header row and costs 0 credits.
  • Duplicates merged across providers are free. You pay once per unique job.
  • To preview cost first, send the same body with "dry_run": true. The response has estimate.credits_range and costs nothing. See Credits and billing.

Change BETTERJOBS_URL to https://api.betterjobs.cc/v1/sandbox/jobs/search and remove the key check and the Authorization header. The sandbox returns fixed illustrative data and charges nothing. It is good for laying out the sheet before you spend credits. Switch back to the live URL when you are done.

  • One cell returns at most 100 jobs, the maximum page size. For more, use an async search from code and import the result as CSV.
  • Apps Script stops a custom function after 30 seconds. The API’s default timeout_ms is 10 seconds, so a search finishes well within that. Slow providers are dropped and the result is partial.
  • Many cells recalculating at once can hit your rate limit (429 rate_limited). Keep the number of live formulas small. See Rate limits.