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.
Set it up
Section titled “Set it up”-
In your sheet, open Extensions → Apps Script.
-
Open Project Settings (the gear icon). Under Script properties, add a property named
BETTERJOBS_API_KEYwith your key as the value. The key stays out of the code and out of the sheet. -
Back in the editor, replace the contents of
Code.gswith the script below and save. -
In any cell, type
=BETTERJOBS_SEARCH(...)as shown above.
The script
Section titled “The script”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.
Control what it costs
Section titled “Control what it costs”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 hasestimate.credits_rangeand costs nothing. See Credits and billing.
Try it without a key
Section titled “Try it without a key”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.
Limits
Section titled “Limits”- 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_msis 10 seconds, so a search finishes well within that. Slow providers are dropped and the result ispartial. - Many cells recalculating at once can hit your rate limit (
429 rate_limited). Keep the number of live formulas small. See Rate limits.
Related
Section titled “Related”- Find companies hiring for the filters behind this formula.
- Filters for
seniority_or,remoteand the rest. Add them tobody.filtersin the script. - Troubleshooting if the sheet stays empty.