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.

  1. Contact Greenhouse Partner Support: Reach out via [email protected]
  2. Provide Information: Tell us:
    • Integration Name: What is your product/integration called?
    • Environment: Is this for testing (only your own sandbox) or production (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)

  1. 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)
  2. Build the Authorization URL: Link your button to the Greenhouse authorization endpoint: https://auth.greenhouse.io/authorize
  3. 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 your client_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 provided redirect_uri doesn't match the one registered for your client_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 than code).
    • ?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 and error_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.

  1. Create an Endpoint: Build the endpoint in your application corresponding to your redirect_uri.
  2. Check for Errors: Before proceeding, check if the redirect URL contains error and error_description query parameters. Handle these appropriately (e.g., display a message to the user).
  3. Verify the state (If used): If no error parameters are present, check that the state query parameter returned by Greenhouse matches the unique state 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.
  4. 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!

  1. Make a POST Request: Immediately send a server-to-server POST request to the Greenhouse token endpoint: https://auth.greenhouse.io/token
  2. Include Headers:
    • Authorization: Basic <BASE64_ENCODED_CREDENTIALS>
      • How to generate: Combine your Client ID and Client Secret with a colon (:), like your_client_id_123:your_client_secret_xyz. Then, Base64 encode this entire string. The specific function depends on your programming language.
  3. 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 if scope 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

  1. Store Securely: Securely store the access_token and refresh_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.
  2. Redirect User: Redirect the user within your application to a success page confirming the connection is complete.
  3. (Inform Greenhouse - If Required): Once your callback endpoint (redirect_uri) is operational and tested, please inform Greenhouse support via [email protected] and provide the final redirect_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).

  1. Make a POST Request: Send a server-to-server POST request to the Greenhouse token endpoint: https://auth.greenhouse.io/token
  2. Include Headers:
    • Authorization: Basic <BASE64_ENCODED_CREDENTIALS> (Same as in Step 4)
  3. 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.

  1. 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.