Integrating as a Greenhouse Harvest Partner (OAuth 2.0 Auth Code Grant Guide)
This guide outlines how partner integrations can connect to the Greenhouse Harvest V3 API using the standard OAuth 2.0 Authorization Code Grant flow. You'll learn how to get authorization from mutual customers, obtain access tokens, keep the connection active, and handle common errors.
(Reference: This process follows the OAuth 2.0 standard described in RFC 6749, Section 4.1).
Key Token Lifetimes (Time-To-Live or TTL)
- Authorization Code: 1 minute (Must be exchanged quickly!)
- Access Token: 1 hour (Used to make API calls)
- Refresh Token: 24 hours (Used to get new access tokens)
Step 1: Get Your Client Credentials
Before you start, you need API credentials from Greenhouse.
- Contact Greenhouse Partner Support: Reach out via
[email protected]
- Provide Information: Tell us:
- Integration Name: What is your product/integration called?
- Environment: Is this for
testing
(only your own sandbox) orproduction
(live for mutual customers)? - Required Scopes: Which specific data permissions does your integration need (e.g.,
harvest:job_posts:list
,harvest:candidates:list
)? See Harvest API documentation for available scopes. - Redirect URI: The url where you handle the authorization code callback (see step 3)
- Logo: Provide a 128x128 logo file. Customers will see this when authorizing your integration.
Greenhouse will provide you with a Client ID
and a Client Secret
. Keep your Client Secret confidential!
Step 2: Initiate the Connection (Redirect User to Greenhouse)
- Create a "Connect" Button: Add a button or link in your application labelled something like "Connect with Greenhouse".
- (Optional: If you need Greenhouse logos or branding guidelines, please consult
https://brand.greenhouse.io
)
- (Optional: If you need Greenhouse logos or branding guidelines, please consult
- Build the Authorization URL: Link your button to the Greenhouse authorization endpoint:
https://auth.greenhouse.io/authorize
- Add Query Parameters: Include the following parameters in the URL:
response_type=code
(Required: Tells Greenhouse you're using the Authorization Code flow)client_id=<YOUR_CLIENT_ID>
(Required: The Client ID Greenhouse gave you)redirect_uri=<YOUR_CALLBACK_URL>
(Required: The URL in your application where Greenhouse will send the user back after authorization. Must match the one registered with yourclient_id
.)scope=<SCOPES>
(Required: A space-separated list of the permissions you need, e.g.,harvest:job_posts:list harvest:candidates:list
)state=<RANDOM_STRING>
(Optional but Recommended: A unique, unpredictable string you generate. You'll verify this later to prevent CSRF attacks).
Example Authorization Link:
https://auth.greenhouse.io/authorize?
response_type=code
&client_id=your_client_id_123
&redirect_uri=https://your-app.com/greenhouse/callback
&scope=harvest:job_posts:list+harvest:candidates:list
&state=csrf_prevention_token_abc987
Potential Errors During Authorization Request:
- Direct JSON Errors (HTTP 400 Bad Request): If there's a fundamental issue with your client configuration before redirection can occur, Greenhouse may respond directly to the customer with:
{"error": "invalid_request", "error_description": "'client_id=...' is invalid"}
(Client ID is invalid or revoked).{"error": "unauthorized_client", "error_description": "'client_id=...' is not allowed to perform the authorization code grant"}
(Client ID is not configured for this OAuth flow).{"error": "invalid_request", "error_description": "'redirect_uri=...' is not configured for 'client_id=...'"}
(The providedredirect_uri
doesn't match the one registered for yourclient_id
).
- Redirect Errors: If the initial client check passes but other issues arise, Greenhouse will redirect the user back to your
redirect_uri
with error details in the query parameters:?error=unsupported_response_type&error_description="'response_type=...' is not supported"
(You used something other thancode
).?error=invalid_scope&error_description="'scope=...' is invalid for 'client_id=...'"
(You requested scopes not permitted for your client application).- (Other errors may be returned via redirect, typically including
error
anderror_description
parameters).
Step 3: Handle the Callback (Receive Authorization Code)
When the user successfully authenticates and authorizes your integration on Greenhouse (or if an error occurs after initial validation - see Step 2 errors), they will be redirected back to the redirect_uri
you specified.
- Create an Endpoint: Build the endpoint in your application corresponding to your
redirect_uri
. - Check for Errors: Before proceeding, check if the redirect URL contains
error
anderror_description
query parameters. Handle these appropriately (e.g., display a message to the user). - Verify the
state
(If used): If no error parameters are present, check that thestate
query parameter returned by Greenhouse matches the uniquestate
value you generated for this user's session in Step 2. If they don't match, reject the request (do not proceed to exchange the code) as it might be malicious or outdated. - Extract the Authorization Code: If no errors occurred and the state is valid, get the
code
query parameter from the incoming request URL. This is the single-use Authorization Code.
Example Successful Redirect Back to Your App:
https://your-app.com/greenhouse/callback?
code=auth_code_received_from_greenhouse
&state=csrf_prevention_token_abc987
Example Error Redirect Back to Your App:
https://your-app.com/greenhouse/callback?
error=invalid_scope
&error_description=Scope%20xyz%20is%20invalid
&state=csrf_prevention_token_abc987
Step 4: Exchange Authorization Code for Tokens
IMPORTANT: You have only 1 minute to exchange the code
before it expires!
- Make a POST Request: Immediately send a server-to-server POST request to the Greenhouse token endpoint:
https://auth.greenhouse.io/token
- Include Headers:
Authorization: Basic <BASE64_ENCODED_CREDENTIALS>
- How to generate: Combine your
Client ID
andClient Secret
with a colon (:
), likeyour_client_id_123:your_client_secret_xyz
. Then, Base64 encode this entire string. The specific function depends on your programming language.
- How to generate: Combine your
- Include Query Parameters:
grant_type=authorization_code
code=<AUTHORIZATION_CODE>
(The code you received in Step 3)
Example Request (curl
):
curl \
--location \
--request POST 'https://auth.greenhouse.io/token?grant_type=authorization_code&code=auth_code_received_from_greenhouse' \
--header 'Authorization: Basic [Your Base64 Encoded ClientID:ClientSecret]' \
--data ''
Expected Successful Response (JSON):
{
"token_type": "Bearer",
"access_token": "received_access_token_string...",
"refresh_token": "received_refresh_token_string...",
"expires_at": "iso8601_datetime_string..." // Indicates when the access_token expires
}
Potential Errors During Token Exchange:
Most errors during token exchange return an HTTP 400 Bad Request with a JSON body like: { "message": "Bad Request Params", "errors": ["Specific error description"] }
Common errors
include:
"grant_type=... is invalid, please use one of: authorization_code, refresh_token"
"Client application cannot perform grant_type=..., please use one of: authorization_code, refresh_token"
"Authorization code does not exist"
"Authorization code has been invalidated at ..."
"Authorization code has already been exchanged for new tokens"
"Authorization code expired at .... The user must re-authorize consent"
(Occurs if you take longer than 1 minute)"Authorization code is assigned to a disabled user"
"Client application is not requesting any scopes"
(Should not typically happen ifscope
was sent in Step 2)
One specific error returns an HTTP 401 Unauthorized: { "message": "Unauthorized", "errors": ["Client application is not authorized to access 1 or more of the requested scopes"] }
- This occurs if the client tries to exchange a code associated with scopes it isn't permitted to request.
Handle these errors by logging the issue and potentially informing the user they may need to reconnect or contact partner support. If the code expired, prompt the user to restart the connection flow (Step 2).
Step 5: Store Tokens and Confirm Success
- Store Securely: Securely store the
access_token
andrefresh_token
associated with the user or customer account in your system (e.g., encrypted in your database). Never store these tokens in client-side code. You will need them for future API calls and refreshing. - Redirect User: Redirect the user within your application to a success page confirming the connection is complete.
- (Inform Greenhouse - If Required): Once your callback endpoint (
redirect_uri
) is operational and tested, please inform Greenhouse support via[email protected]
and provide the finalredirect_uri
.
Step 6: Maintain Access (Refreshing the Access Token)
Access tokens expire after 1 hour. Use the refresh_token
(which lasts 24 hours) to get a new access token before the current one expires.
IMPORTANT: If the refresh_token
expires (not used within 24 hours), the user must repeat the entire authorization flow from Step 2. To avoid this disruption, refresh proactively (e.g., when the access token is nearing expiry or when you receive a 401 error indicating an expired token during an API call).
- Make a POST Request: Send a server-to-server POST request to the Greenhouse token endpoint:
https://auth.greenhouse.io/token
- Include Headers:
Authorization: Basic <BASE64_ENCODED_CREDENTIALS>
(Same as in Step 4)
- Include Query Parameters:
grant_type=refresh_token
refresh_token=<STORED_REFRESH_TOKEN>
(The refresh token you stored)
Example Request (curl
):
curl \
--location \
--request POST 'https://auth.greenhouse.io/token?grant_type=refresh_token&refresh_token=stored_refresh_token_string' \
--header 'Authorization: Basic [Your Base64 Encoded ClientID:ClientSecret]' \
--data ''
Expected Successful Response (JSON):
{
"token_type": "Bearer",
"access_token": "new_access_token_string...",
"refresh_token": "new_refresh_token_string...", // You get a new refresh token too!
"expires_at": "iso8601_datetime_string..."
}
Action: Replace the old access_token
and refresh_token
in your storage with the new ones received in the response.
Potential Errors During Token Refresh:
Similar to Step 4, most errors during refresh return an HTTP 400 Bad Request with a JSON body: { "message": "Bad Request Params", "errors": ["Specific error description"] }
Common errors
include:
"grant_type=... is invalid, please use one of: authorization_code, refresh_token"
"Client application cannot perform grant_type=..., please use one of: authorization_code, refresh_token"
"Refresh token does not exist"
"Refresh token has been invalidated at ..."
"Refresh token has already been exchanged for new tokens"
"Refresh token expired at .... The user must re-authorize consent"
(Occurs if the refresh token wasn't used within 24 hours)"Refresh token is assigned to a disabled user"
If you receive an error indicating the refresh token is invalid or expired, you must prompt the user to go through the authorization flow again starting from Step 2.
Step 7: Access the Harvest V3 API
Use the stored access_token
to make calls to the Greenhouse Harvest V3 API.
- Include Authorization Header: Add the following header to your API requests:
Authorization: Bearer <STORED_ACCESS_TOKEN>
Example (Conceptual):
GET /v3/job_posts HTTP/1.1
Host: harvest.greenhouse.io
Authorization: Bearer stored_access_token_string...
Potential Errors During API Access:
- HTTP 401 Unauthorized: Often indicates the
access_token
has expired. Attempt to refresh it using Step 6. If refreshing fails or you still get 401s, the user may need to re-authorize. - HTTP 403 Forbidden: This typically relates to permissions:
- The user who authorized the connection may not have the necessary Greenhouse permissions in-app to perform the requested action (e.g., creating a candidate, accessing certain data).
- All list endpoints require authorization by a Greenhouse user with Site Admin privileges. If a non-Site Admin authorized the connection, attempts to access these specific endpoints will result in a 403.
Refer to the official Greenhouse Harvest V3 API documentation for details on available endpoints, request/response formats, specific scopes, and potential API-specific errors.
Updated 5 days ago