# <a name="main-title"></a> Idealist Listings API documentation ## <a name="target-audience"></a> Target Audience This documentation is aimed at software developers wishing to implement programmatic fetching of listings posted on Idealist.org. ## <a name="overview"></a> Overview Idealist.org provides the Listings API to consume listings posted on Idealist.org. The API is a JSON-based REST API. You'll need an API token. ## <a name="support"></a> Support You must contact the Idealist support staff to obtain an API token: <support@idealist.org>. For technical questions regarding the API during the implementation phase, contact <api@idealist.org>. For support afterward, contact <support@idealist.org>. ## <a name="quickstart"></a> Quickstart Here's an example of Python code that would fetch all the full-time jobs. It's going to fetch jobs from `https://www.idealist.org/api/v1/listings/jobs` and also get job details `https://www.idealist.org/api/v1/listings/jobs/{job_id}`. ```python import requests import time API_TOKEN = "your token here" headers = {"Accept": "application/json"} # start by discovering all the job IDs job_ids = set() url = "https://www.idealist.org/api/v1/listings/jobs" has_more = True ttl = 200 # to avoid looping forever in case something is wrong in the code while has_more and ttl > 0: ttl -= 1 print("GET", url) response = requests.get(url, headers=headers, auth=(API_TOKEN, "")) assert response.status_code == 200 data = response.json() print("Received", len(data["jobs"]), "jobs") for job in data["jobs"]: job_ids.add(job["id"]) has_more = data["hasMore"] since = data["jobs"][-1]["updated"] url = f"https://www.idealist.org/api/v1/listings/jobs?since={since}" time.sleep(1) # politeness print("Discovered", len(job_ids), "jobs") MOST_RECENT_UPDATE = data["jobs"][-1]["updated"] # store that value somewhere for later print("Most recent update", MOST_RECENT_UPDATE) # now that we know all the job IDs, we can fetch more details about the jobs jobs = [] for job_id in job_ids: url = f"https://www.idealist.org/api/v1/listings/jobs/{job_id}" print("GET", url) response = requests.get(url, headers=headers, auth=(API_TOKEN, "")) if response.status_code == 200: # job may have been unpublished since we got the ID jobs.append(response.json()["job"]) time.sleep(1) # politeness ``` Then, a few moments later (daily, hourly, etc.), we can fetch only the jobs that were updated since last time we queried the API: ```python since = MOST_RECENT_UPDATE # get that value from where you stored it above job_ids = set() url = f"https://www.idealist.org/api/v1/listings/jobs?since={since}" has_more = True ttl = 200 # to avoid looping forever in case something is wrong in the code while has_more and ttl > 0: ttl -= 1 print("GET", url) response = requests.get(url, headers=headers, auth=(API_TOKEN, "")) assert response.status_code == 200 data = response.json() print("Received", len(data["jobs"]), "jobs") for job in data["jobs"]: job_ids.add(job["id"]) has_more = data["hasMore"] since = data["jobs"][-1]["updated"] url = f"https://www.idealist.org/api/v1/listings/jobs?since={since}" time.sleep(1) # politeness # job_ids should contain only a few jobs instead of a few thousands print("Discovered", len(job_ids), "jobs") ``` ## <a name="authentication"></a> Authentication To authenticate all API endpoint requests, you must obtain a production API token from the Idealist support staff -- <support@idealist.org> You must use the API token as the `username` for Basic HTTP authentication. Use an empty string for the `password`. Most HTTP libraries come with Basic authentication. For example, using the Python library `requests` (<http://python-requests.org>): ```python import requests API_TOKEN = "9e11d62224ab48c79432fae431397620" url = "https://www.idealist.org/api/v1/listings/jobs" headers = { "Accept": "application/json", } response = requests.get(url, auth=(API_TOKEN, ""), headers=headers) jobs = response.json()["jobs"] print(jobs) ``` For more information: <https://en.wikipedia.org/wiki/Basic_access_authentication>. ## <a name="rate-limiting"></a> Throttling / Rate Limiting There's currently no implementation of any rate limiting. However, we expect API users to respect our resources and not do any requests in parallel. It's also encouraged to wait at least a few milliseconds (250ms is great) between requests. Lastly, the API is provided with the hope that clients will store the value of `since` they should use next time they fetch listings, and not fetch all the listings from scratch every time they want to get the latest listings. ## <a name="unpublished-listings"></a> Unpublished Listings In the [Quickstart](#quickstart) example above, the listings returned were only the published ones. If you need to know when listings are unpublished, you can add the parameter `includeUnpublished=true` when fetching the list of listings. Note that this parameter is only taken into account when also using the parameter `since`. For example: ```python since = MOST_RECENT_UPDATE # get that value from where you stored it above job_ids = set() unpublished_job_ids = set() url = f"https://www.idealist.org/api/v1/listings/jobs?includeUnpublished=true&since={since}" has_more = True ttl = 200 # to avoid looping forever in case something is wrong in the code while has_more and ttl > 0: ttl -= 1 print("GET", url) response = requests.get(url, headers=headers, auth=(API_TOKEN, "")) assert response.status_code == 200 data = response.json() print("Received", len(data["jobs"]), "jobs") for job in data["jobs"]: if job["isPublished"]: job_ids.add(job["id"]) else: unpublished_job_ids.add(job["id"]) has_more = data["hasMore"] since = data["jobs"][-1]["updated"] url = f"https://www.idealist.org/api/v1/listings/jobs?since={since}" time.sleep(1) # politeness # job_ids should contain only a few jobs instead of a few thousands print("Discovered", len(job_ids), "jobs") print(len(unpublished_job_ids), "jobs are unpublished") ``` Here's a sample response: ```json { "jobs": [{ "id": "2f1cf635d6b14c9587a83a7375ea1412", "firstPublished": "2019-06-23T18:02:07.212112Z", "updated": "2019-06-23T18:02:07.269921Z", "name": "Executive Director", "url": { "en": "https://www.idealist.org/en/nonprofit-job/...", "es": "https://www.idealist.org/es/ong-empleo/...", "pt": "https://www.idealist.org/pt/ong-emprego/..." }, "isPublished": true }, { "id": "56a98590617e458c967e7b3633d40302", "firstPublished": "2019-06-23T18:02:09.093284Z", "updated": "2019-06-23T18:02:08.283617Z", "name": null, "url": { "en": null, "es": null, "pt": null }, "isPublished": false }, ... ], "hasMore": true } ``` ## <a name="endpoints"></a> API Reference - Endpoints All endpoints are hosted on `https://www.idealist.org`. For example, when the docs show `GET /api/v1/listings/jobs`, it means you should do a `GET` request to `https://www.idealist.org/api/v1/listings/jobs`. ### <a name="jobs"></a> Jobs `GET /api/v1/listings/jobs` Sample response: ```json { "jobs": [{ "id": "2f1cf635d6b14c9587a83a7375ea1412", "firstPublished": "2019-06-23T18:02:07.212112Z", "updated": "2019-06-23T18:02:07.269921Z", "name": "Executive Director", "url": { "en": "https://www.idealist.org/en/nonprofit-job/...", "es": "https://www.idealist.org/es/ong-empleo/...", "pt": "https://www.idealist.org/pt/ong-emprego/..." }, "isPublished": true }, ... ], "hasMore": true } ``` It returns up to 100 jobs, ordered by oldest updated first. To page through all the jobs, you can pass `since` in the URL like so: `GET /api/v1/listings/jobs?since=2019-06-23T18:02:07.269921Z` using the last job's `updated` value. For example, in Python: ```python import requests import time job_ids = set() API_TOKEN = "your token here" url = "https://www.idealist.org/api/v1/listings/jobs" headers = {"Accept": "application/json"} has_more = True ttl = 200 # to avoid looping forever in case something is wrong in the code while has_more and ttl > 0: ttl -= 1 response = requests.get(url, headers=headers, auth=(API_TOKEN, "")) data = response.json() for job in data["jobs"]: job_ids.add(job["id"]) has_more = data["hasMore"] since = data["jobs"][-1]["updated"] url = f"https://www.idealist.org/api/v1/listings/jobs?since={since}" time.sleep(1) # politeness ``` Note that `since` is inclusive, meaning that you should expect to get the last job again as the first job in the next request. `GET /api/v1/listings/jobs` A request with many parameters looks like this: `GET /api/v1/listings/jobs?since=2019-06-23T18:02:07.269921Z` ### <a name="job-details"></a> Job Details `GET /api/v1/listings/jobs/{job_id}` Sample response: ```json { "job": { "id": "f97678360b1fc0268a63e42871d72760", "firstPublished": "2019-07-23T17:34:23.848076Z", "updated": "2019-07-23T17:34:23.877984Z", "name": "Executive Director", "description": "<p>The description of the job is some richtext.</p>", "expires": "2019-08-22T17:34:23.848076Z", "org": { "id": "f5f56cb69a66468ecc52c9bbb16e99d6", "name": "Example Organization", "url": { "en": "https://idealist.org/en/nonprofit/...", "es": "https://idealist.org/es/ong/...", "pt": "https://idealist.org/pt/ong/..." }, "logo": "https://imagehost.com/f5f56cb69a66468ecc52c9bbb16e99d6-logo.png", "areasOfFocus": ["IMMIGRANTS_OR_REFUGEES", "LEGAL_ASSISTANCE"], "orgType": "NONPROFIT", "is501c3": false, "isBCorp": false, "isSocialEnterprise": false, "locale": "en", "governmentId": "abcdef", "address": { "full": "123 Broadway, New York, NY, United States", "line1": "123 Broadway", "line2": null, "city": "New York", "state": "New York", "stateCode": "NY", "zipcode": "10000", "country": "US", "latitude": 41.707002, "longitude": -75.010003 } }, "address": { "full": "123 Broadway, New York, NY, United States", "line1": "123 Broadway", "line2": null, "city": "New York", "state": "New York", "stateCode": "NY", "zipcode": "10000", "country": "US", "latitude": 41.707002, "longitude": -75.010003, "cityOnly": false }, "applyEmail": "apply@example.org", "applyUrl": "https://example.org/", "applyText": "<p>Some rich text</p>", "applyOnIdealist": false, "benefits": "<p>Comprehensive benefit package</p>", "salaryMinimum": "50000.00", "salaryMaximum": null, "salaryCurrency": "USD", "salaryPeriod": "YEAR", "salaryDetails": "Based on experience", "professionalLevel": "PROFESSIONAL", "languageProficiency": null, "education": "FOUR_YEAR_DEGREE", "educationDetails": null, "applicationDeadline": "2019-09-17", "startDate": "2019-10-01", "endDate": null, "isFullTime": true, "isTemporary": false, "isContract": true, "locationType": "REMOTE", "remoteZone": "CITY", "remoteCountry": null, "functions": ["ADMINISTRATIVE", "LEGAL"], "areasOfFocus": ["IMMIGRANTS_OR_REFUGEES", "LEGAL_ASSISTANCE"], "url": { "en": "https://www.idealist.org/en/nonprofit-job/...", "es": "https://www.idealist.org/es/ong-empleo/...", "pt": "https://www.idealist.org/pt/ong-emprego/..." } } } ``` ### <a name="internships"></a> Internships `GET /api/v1/listings/internships` Sample response: ```json { "internships": [{ "id": "0b6c1534f850452d855a646d203f617d", "firstPublished": "2019-06-24T10:07:42.822915Z", "updated": "2019-06-24T10:07:42.825251Z", "name": "Executive Director Intern", "url": { "en": "https://www.idealist.org/en/nonprofit-internship/...", "es": "https://www.idealist.org/es/ong-pasantia/...", "pt": "https://www.idealist.org/pt/ong-estagio/..." }, "isPublished": true }, ... ], "hasMore": true } ``` It works in the same manner as <a href="#jobs">jobs</a>: up to 100 are returned, and paging is done using `since` with the last (most recently) updated internship. ### <a name="internship-details"></a> Internship Details `GET /api/v1/listings/internships/{internship_id}` Sample response: ```json { "internship": { "id": "d6d4dc8ab6c34346a160554442d55a65", "firstPublished": "2019-07-23T18:01:43.119618Z", "updated": "2019-07-23T18:01:43.120519Z", "name": "Communications Intern", "description": "<p>Rich text description</p>", "expires": "2019-08-22T18:01:43.119618Z", "org": { "id": "f5f56cb69a66468ecc52c9bbb16e99d6", "name": "Example Organization", "url": { "en": "https://idealist.org/en/nonprofit/...", "es": "https://idealist.org/es/ong/...", "pt": "https://idealist.org/pt/ong/..." }, "logo": "https://imagehost.com/f5f56cb69a66468ecc52c9bbb16e99d6-logo.png", "areasOfFocus": ["IMMIGRANTS_OR_REFUGEES", "LEGAL_ASSISTANCE"], "orgType": "NONPROFIT", "is501c3": false, "isBCorp": false, "isSocialEnterprise": false, "locale": "en", "governmentId": "abcdef", "address": { "full": "123 Broadway, New York, NY, United States", "line1": "123 Broadway", "line2": null, "city": "New York", "state": "New York", "stateCode": "NY", "zipcode": "10000", "country": "US", "latitude": 41.707002, "longitude": -75.010003 } }, "address": { "full": "123 Broadway, New York, NY, United States", "line1": "123 Broadway", "line2": null, "city": "New York", "state": "New York", "stateCode": "NY", "zipcode": "10000", "country": "US", "latitude": 41.707002, "longitude": -75.010003, "cityOnly": false }, "applyEmail": "resumes@example.org", "applyUrl": "http://example.org/page-url", "applyText": "<p>Rich text on how to apply.</p>", "applyOnIdealist": false, "benefits": null, "paid": false, "wage": null, "academicCredits": false, "paymentOk": true, "locationType": "ONSITE", "remoteZone": null, "remoteCountry": null, "hoursPerWeek": "20", "isCovid": false, "isPostedAnonymously": false, "applicationDeadline": "2019-08-16", "startDate": null, "endDate": "2019-12-13", "functions": ["ADVOCACY", "MARKETING_COMMUNICATIONS", "RESEARCH", "SOCIAL_MEDIA", "WRITING_JOURNALISM"], "url": { "en": "https://www.idealist.org/en/nonprofit-internship/...", "es": "https://www.idealist.org/es/ong-pasantia/...", "pt": "https://www.idealist.org/pt/ong-estagio/..." } } } ``` ### <a name="volops"></a> Volunteer Listings `GET /api/v1/listings/volops` Sample response: ```json { "volops": [{ "id": "0b6c1534f850452d855a646d203f617d", "firstPublished": "2019-06-24T10:07:42.822915Z", "updated": "2019-06-24T10:07:42.825251Z", "name": "Volunteer Driver", "url": { "en": "https://www.idealist.org/en/volunter-opportunity/...", "es": "https://www.idealist.org/es/oportunidad-voluntariado/...", "pt": "https://www.idealist.org/pt/oportunidade-voluntariado/..." }, "isPublished": true }, ... ], "hasMore": true } ``` It works in the same manner as <a href="#jobs">jobs</a>: up to 100 are returned, and paging is done using `since` with the last (most recently) updated volop. ### <a name="volop-details"></a> Volunteer Listing Details `GET /api/v1/listings/volops/{volop_id}` Sample response: ```json { "volop": { "id": "d6d4dc8ab6c34346a160554442d55a65", "firstPublished": "2019-07-23T18:01:43.119618Z", "updated": "2019-07-23T18:01:43.120519Z", "name": "Communications Intern", "description": "<p>Rich text description</p>", "expires": "2019-08-22T18:01:43.119618Z", "org": { "id": "f5f56cb69a66468ecc52c9bbb16e99d6", "name": "Example Organization", "url": { "en": "https://idealist.org/en/nonprofit/...", "es": "https://idealist.org/es/ong/...", "pt": "https://idealist.org/pt/ong/..." }, "logo": "https://imagehost.com/f5f56cb69a66468ecc52c9bbb16e99d6-logo.png", "areasOfFocus": ["IMMIGRANTS_OR_REFUGEES", "LEGAL_ASSISTANCE"], "orgType": "NONPROFIT", "is501c3": false, "isBCorp": false, "isSocialEnterprise": false, "locale": "en", "governmentId": "abcdef", "address": { "full": "123 Broadway, New York, NY, United States", "line1": "123 Broadway", "line2": null, "city": "New York", "state": "New York", "stateCode": "NY", "zipcode": "10000", "country": "US", "latitude": 41.707002, "longitude": -75.010003 } }, "locale": "en", "isVirtual": false, "isCovid": false, "isPostedAnonymously": false, "address": { "full": "123 Broadway, New York, NY, United States", "description": "Town Hall", "line1": "123 Broadway", "line2": null, "city": "New York", "state": "New York", "stateCode": "NY", "zipcode": "10000", "country": "US", "latitude": 41.707002, "longitude": -75.010003, "cityOnly": false }, "directions": "<p>Rich text on directions to get to the location.</p>", "actionType": "VOLOP", "actionSubType": null, "actionTypeText": null, "applyEmail": "resumes@example.org", "applyUrl": "http://example.org/page-url", "applyText": "<p>Rich text on how to apply.</p>", "applyOnIdealist": false, "benefits": null, "paid": false, "wage": null, "academicCredits": false, "paymentOk": true, "locationType": "REMOTE", "remoteZone": "CITY", "remoteCountry": null, "welcomeGroups": true, "welcomeFamilies": true, "welcomeTeens": true, "welcomeIntl": true, "welcomeAge55Plus": true, "welcomePrivateCorpGroups": true, "detailsStipendProvided": true, "detailsTrainingProvided": true, "detailsHousingAvailable": true, "detailsCulturalSupport": true, "detailsWheelchairAccessible": true, "orientationRequired": true, "backgroundCheckRequired": true, "driversLicenseRequired": true, "feeRequired": true, "feeAmount": "25.00", "isRecurring": true, "timesOfDay": "WEEKDAYS", "expectedTime": "FEW_HOURS_WEEK", "startDate": null, "endDate": "2019-12-13", "startTime": null, "endTime": "02:00:00", "timezone": "America/New_York", "starts": null, "ends": "2019-12-13T06:00:00Z", "ageRequirement": 18, "otherRequirements": "Can lift heavy things", "functions": ["LANGUAGES", "MENTOR_TUTOR", "PUBLIC_SPEAKING"], "areasOfFocus": ["IMMIGRANTS_OR_REFUGEES", "LEGAL_ASSISTANCE"], "url": { "en": "https://www.idealist.org/en/volunter-opportunity/...", "es": "https://www.idealist.org/es/oportunidad-voluntariado/...", "pt": "https://www.idealist.org/pt/oportunidade-voluntariado/..." } } } ``` For the complete list of volunteer `functions`, see https://www.idealist.org/api/v1/volop-functions. For the complete list of `areasOfFocus`, see https://www.idealist.org/api/v1/areas-of-focus. Note that Volunteer listing details are subject to change. For example, `isCovid` will hopefully become obsolete soon. ## <a name="status-codes"></a> API Reference - Status Codes The API will return the following HTTP status codes: - 200 OK - 302 Found - if you are being redirected to /login, make sure that you are passing `Accept: application/json` - 401 Unauthorized - check your API token - 404 Not Found - It can mean a job has expired, has been hidden, etc. Otherwise, make sure the URL is correct. ## <a name="changelog"></a> Changelog - 2025-04-25 Add links to volunteer functions and areas of focus. - 2025-04-20 Reformat code examples. - 2025-02-25 Added `welcomeTeens`, `welcomeAge55Plus`, `welcomePrivateCorpGroups`, `orientationRequired`, `backgroundCheckRequired`, `driversLicenseRequired`, `ageRequirement`, `otherRequirements` to volop. - 2025-02-25 Added `areasOfFocus` to jobs and volops. - 2025-02-14 Remove `isFullTime` filter. - 2024-06-11 Added `salaryCurrency`, `salaryPeriod` and `endDate`. - 2023-12-06 Added `applyOnIdealist`. - 2022-10-10 Remove `remoteOk` and `remoteTemporary`, to replace it with `locationType`. - 2022-08-17 Added `includeUnpublished`. Removed `groups` from `volops`. - 2022-02-08 Added `address.full` and more details about `org`. - 2021-10-08 Added `remoteZone`, `remoteCountry`, and `remoteTemporaryCovid` to api examples. - 2021-09-05 Added org urls and logos. - 2020-08-11 Added Volunteer listings. - 2019-07-23 Initial version.