How to Integrate with the Lever API (2026 Engineering Guide)
A technical guide for engineering teams integrating the Lever API. Covers OAuth 2.0 token lifecycle, offset pagination, rate limits, webhooks, and the Opportunity data model.
If you're asking "how do I integrate with the Lever API?" here's the straight answer: you're signing up to manage a full OAuth 2.0 lifecycle with one-hour token expiry, implement cursor-based pagination that doesn't behave like any other ATS, respect a 10 requests-per-second rate limit that drops to 2 req/sec for application submissions, and navigate a data model where the deprecated Candidates endpoints still exist alongside the Opportunities endpoints you should actually be using.
Your enterprise deal is blocked because the prospect's talent acquisition team won't adopt a tool that can't plug into their ATS. Your engineering lead says it's a two-week project. If you've been through this before with HRIS integrations or CRM integrations, you already know why that estimate is wrong.
The initial HTTP request to fetch an opportunity takes an afternoon. Managing OAuth token rotation, parsing non-standard offset tokens, building rate-limit-aware retry logic, and maintaining the integration when Lever deprecates endpoints — that's where the real weeks go.
This guide breaks down exactly what it takes to build a reliable Lever integration: the specific API surfaces, the authentication gotchas, the pagination traps, and how to ship without burning your team's entire quarter.
The Business Case for a Lever API Integration
Integrating with Lever is a revenue-blocking requirement for B2B SaaS companies selling into mid-market and enterprise recruiting teams.
The ATS market is not slowing down. According to MarketsandMarkets, the global applicant tracking system market was valued at USD 3.28 billion in 2025 and is projected to reach USD 4.88 billion by 2030, growing at a CAGR of 8.2%. More companies purchasing ATS platforms means more prospects expecting your B2B SaaS product to work with whichever system they've chosen.
Lever combines ATS and CRM into a single platform called LeverTRM, which is popular with mid-market, sourcing-heavy teams. Companies like Netflix, Shopify, and thousands of growth-stage startups rely on it. If your product touches the employee lifecycle — background checks, technical assessments, onboarding automation, identity provisioning — you will inevitably be asked for a Lever integration.
But your prospects aren't all on the same ATS. You'll likely need to support Greenhouse, Workable, and Ashby alongside Lever. As we discussed in our guide on how to integrate multiple ATS platforms, HR tech stacks are notoriously fragmented. Building a one-off custom script just for Lever creates technical debt that multiplies the moment your sales team requests the next integration. You need an architecture that scales, which is why building native HRIS integrations requires careful planning from day one.
Understanding Lever API Authentication (OAuth vs. API Keys)
Lever supports two authentication methods, and choosing the wrong one will cost you weeks of rework.
API Keys (Basic Auth)
If you're building an internal workflow for your own company's Lever instance, you can generate an API key from the Lever dashboard. Lever uses Basic Authentication, where the API key serves as the username and the password field is left blank.
curl https://api.lever.co/v1/opportunities \
-u "YOUR_API_KEY:"While simple, this approach is entirely unsuited for B2B SaaS. You cannot ask enterprise customers to generate an API key, copy it, and paste it into your application. It violates security policies, breaks when the employee who generated the key leaves the company, and provides no granular scope control.
OAuth 2.0 Authorization Code Flow
For customer-facing B2B integrations, OAuth 2.0 is mandatory — it's what Lever expects for partner integrations. Lever's implementation follows the standard Authorization Code Grant flow, but with Lever-specific requirements that trip up developers:
- Redirect the user to
https://auth.lever.co/authorizewith yourclient_id,redirect_uri,response_type=code, astatetoken for CSRF protection, the requiredaudienceparameter (https://api.lever.co/v1/), and your requested scopes. - Exchange the authorization code for tokens at
https://auth.lever.co/oauth/token. - Use the access token as a Bearer token in the
Authorizationheader.
The audience parameter is a gotcha that catches many developers. Forget it in your sandbox, and things work fine. Forget it in production, and auth silently fails. Nothing in the error response makes it obvious what went wrong.
Token Lifecycle: The One-Hour Cliff
Lever access tokens expire after one hour. Refresh tokens expire after one year or after 90 days of inactivity. You need a proactive refresh strategy — don't wait for a 401 to trigger a refresh. Schedule it ahead of expiry.
import requests
def refresh_lever_token(client_id, client_secret, refresh_token):
"""Proactively refresh a Lever OAuth token before expiry."""
response = requests.post(
"https://auth.lever.co/oauth/token",
data={
"grant_type": "refresh_token",
"client_id": client_id,
"client_secret": client_secret,
"refresh_token": refresh_token,
},
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
if response.status_code == 200:
tokens = response.json()
# Store BOTH the new access_token AND the new refresh_token
return tokens
else:
# Mark account as needs_reauth, notify customer
raise AuthenticationError(f"Lever refresh failed: {response.status_code}")A common mistake: forgetting to persist the new refresh token returned with each exchange. Lever rotates refresh tokens, so the old one becomes invalid immediately after use.
The Refresh Token Race Condition
If your system attempts to refresh a token concurrently across multiple worker threads, Lever will invalidate the grant, forcing your customer to re-authenticate manually. As we cover in our architecture guide to reliable token refreshes, you must implement distributed locks around your token refresh logic.
Truto handles this complexity natively. Instead of writing custom token management logic, Truto refreshes OAuth tokens proactively, shortly before expiry. If a refresh fails, the account is flagged as needing re-authentication and an event fires so your application knows immediately — all driven by declarative configuration rather than hardcoded scripts.
Navigating Lever API Endpoints and Data Models
Lever's data model is candidate-centric, not application-centric. This is the single biggest conceptual difference between Lever and platforms like Greenhouse.
Many legacy ATS platforms treat a candidate and their job application as a single flattened record. Lever takes a more relational approach, which is technically superior but requires more API calls to reconstruct a complete profile.
Core Entities
| Entity | Description | Endpoint |
|---|---|---|
| Contact | A unique person across all opportunities | /contacts/:contact |
| Opportunity | A candidacy for a specific job (the primary object) | /opportunities |
| Application | A posting-specific application within an Opportunity | /opportunities/:id/applications |
| Posting | A job listing | /postings |
| Stage | A step in the hiring pipeline | /stages |
The relationship works like this: one Contact (person) can have many Opportunities (candidacies), and each Opportunity has at most one Application.
erDiagram
CONTACT ||--o{ OPPORTUNITY : "has many"
OPPORTUNITY ||--o| APPLICATION : "has at most one"
APPLICATION }o--|| POSTING : "applied to"
OPPORTUNITY }o--|| STAGE : "currently in"
POSTING }o--o{ REQUISITION : "linked to"The Deprecated Candidates Trap
Here's where it gets messy. The old /candidates endpoints still exist and still return data. But they're deprecated, and Lever explicitly warns you to use /opportunities instead. For any given opportunity, the candidateId you would use for a Candidates endpoint request can be used as the opportunityId in the corresponding Opportunities endpoint. Going forward, the contact field is the unique identifier for an individual person in Lever.
If you build against /candidates today, you're building on deprecated ground. Use /opportunities from day one and track the contact field as your unique person identifier.
This relational model is why building custom integrations is painful. Your application likely just wants a flat "Applicant" record. Truto abstracts this away through its Unified ATS Model, mapping Lever's specific endpoints into a standardized schema. You query a single unified endpoint, and Truto handles the multi-step API orchestration and relationship joining automatically.
Handling Lever API Pagination (The Offset Token)
Lever uses cursor-based pagination with an opaque offset token — not page numbers, not Link headers.
If you attempt to append ?page=2 to a Lever API request, it will be silently ignored. When you make a request to any list endpoint, Lever returns a next attribute containing an offset token if there are more records to fetch. The limit parameter ranges between 1 and 100 items.
{
"data": [...],
"next": "0.1414895548650.a6070140-33db-407c-91f5-2760e15c8e94",
"hasNext": true
}You pass the next value as the offset query parameter in your subsequent request. Here's the pagination loop:
def fetch_all_opportunities(api_key):
"""Paginate through all Lever opportunities."""
all_results = []
offset = None
while True:
params = {"limit": 100}
if offset:
params["offset"] = offset
response = requests.get(
"https://api.lever.co/v1/opportunities",
params=params,
auth=(api_key, ""), # Basic Auth: key as username, blank password
)
data = response.json()
all_results.extend(data["data"])
if not data.get("hasNext"):
break
offset = data["next"]
return all_resultsThe key pitfalls:
- You cannot construct offset tokens yourself. They're opaque. Do not attempt to decode, parse, or manipulate them. Use exactly what the API gives you.
- There's no total count. You don't know how many pages exist until
hasNextreturnsfalse. Plan your UX accordingly. - The default limit varies by endpoint. Always set it explicitly.
Writing custom pagination loops for every third-party API is a massive waste of engineering time. As detailed in our guide on how unified APIs handle pagination differences, this is a solved problem. Truto handles cursor extraction and subsequent request formatting entirely through declarative configuration. Your application simply requests the next page of unified data, and Truto translates that into Lever's specific offset token logic.
Surviving Lever API Rate Limits
Lever enforces a steady-state rate limit of 10 requests per second per API key, with bursts up to 20 requests per second. Application POST requests have a stricter limit of just 2 requests per second — and Lever warns this limit may be changed without notice to maintain system stability.
This is a token bucket implementation. Short bursts are tolerated, but sustained throughput above 10 req/sec triggers 429s.
What This Means in Practice
- Syncing 10,000 opportunities at 100 per page = 100 API calls. At 10 req/sec, that's 10 seconds minimum — assuming zero retries.
- Hydrating opportunity data (fetching related interviews, feedback, and offers per opportunity) multiplies the call count dramatically. Interviews for 10,000 opportunities means another 10,000 calls.
- Application submissions from a custom job site are capped at 2/sec. If you're running a high-traffic careers page, you will hit this. To avoid losing applicants, you must either queue and retry application POST requests that receive a 429 response, or direct candidates to Lever's hosted application form.
Implementing Exponential Backoff
A naive sleep(1) retry strategy will cluster your requests and repeatedly trip the rate limiter. You need exponential backoff with jitter to prevent the thundering herd problem:
import time
import requests
import random
def fetch_with_backoff(url, headers, max_retries=5):
retries = 0
while retries < max_retries:
response = requests.get(url, headers=headers)
if response.status_code == 200:
return response.json()
if response.status_code == 429:
# Exponential backoff: 2^retries + random jitter
sleep_time = (2 ** retries) + random.uniform(0, 1)
print(f"Rate limited. Retrying in {sleep_time:.2f} seconds...")
time.sleep(sleep_time)
retries += 1
elif response.status_code == 503:
# Lever maintenance — back off harder
time.sleep(5 * (retries + 1))
retries += 1
else:
response.raise_for_status()
raise Exception("Max retries exceeded")For a deeper dive into architectural patterns for rate limit handling across multiple vendors, see our guide on handling API rate limits and retries.
Truto handles this automatically. When the platform detects a 429 from Lever, it applies exponential backoff and standardizes the rate-limit response for your client. Your application receives a consistent Retry-After header regardless of whether the underlying API is Lever, Greenhouse, or Workable.
Webhooks: Listening to Real-Time Lever Events
Lever supports webhooks for key pipeline events, including candidate stage changes, hires, archive state changes, and interview lifecycle events.
Polling the /opportunities endpoint every five minutes to check for updates will quickly exhaust your rate limits. Instead, configure Lever webhooks. Available events include candidateHired, candidateStageChange, candidateArchiveChange, candidateDeleted, applicationCreated, and interview CRUD events.
Signature Verification
Lever signs webhook payloads using HMAC-SHA256, but with an unusual twist: the signature is embedded in the POST body, not in an HTTP header. Each payload contains a token, triggeredAt, and signature field. To verify:
- Concatenate the
tokenandtriggeredAtvalues from the payload body. - HMAC-SHA256 the result using your webhook signature token as the key.
- Compare the hex digest to the
signaturefield.
const crypto = require('crypto');
function validateLeverWebhook(body, signatureToken) {
const plainText = body.token + body.triggeredAt;
const hash = crypto
.createHmac('sha256', signatureToken)
.update(plainText)
.digest('hex');
return body.signature === hash;
}Watch out: Lever's webhook signature is in the POST body, not in an HTTP header. If you're used to Stripe or GitHub-style X-Signature headers, you'll need to adjust your verification middleware.
Retry Behavior and HTTPS Requirement
Lever only supports HTTPS-enabled webhook endpoints with valid SSL certificates — self-signed certificates are not supported. When your endpoint returns a non-2xx response, Lever retries the webhook up to five times with increasing delays. Your endpoint must respond with a 2xx status quickly; any response body is ignored.
If your server is down or consistently returns errors, Lever will disable the webhook entirely, silently breaking your integration. You must decouple webhook ingestion from processing — accept the payload, return a 200 immediately, and place the event into a message queue for asynchronous processing.
Build vs. Buy: Shipping Lever Integrations Faster
Everything above describes the work for one ATS. Your prospects aren't all on Lever. Greenhouse uses Basic Auth with Link header pagination. Workable uses a different rate limit scheme entirely. Their data models differ fundamentally: Greenhouse separates Candidates and Applications as distinct entities, while Lever merges them into an Opportunity object. Greenhouse uses integer IDs; Lever uses UUIDs.
There's also a compounding organizational cost. Once you build one ATS connector, sales immediately asks for a second. The moment you build a second, product assumes a third is cheap. By the time you have three, you don't have three integrations — you have the start of an integration platform whether you meant to build one or not.
What a Unified API Abstracts Away
A unified ATS API like Truto maps Lever's Opportunities and Contacts into a standardized schema that works identically for Greenhouse, Workable, Ashby, and others. The Lever-specific quirks — offset token pagination, one-hour token expiry, body-embedded webhook signatures, the deprecated Candidates endpoints — are handled entirely through declarative configuration rather than custom code.
Specifically, Truto handles:
- OAuth token management: Tokens are refreshed proactively, shortly before expiry. If a refresh fails, the account is flagged and an event fires so your application knows immediately.
- Pagination normalization: Lever's offset tokens, Greenhouse's Link headers, and Workable's cursor strategies all resolve to the same pagination interface for your code.
- Rate limit detection: When Lever returns a 429, Truto applies exponential backoff automatically and standardizes the response for your client.
- Data model mapping: Lever's Opportunity/Contact model and Greenhouse's Candidate/Application model both map to a common ATS schema covering Candidates, Applications, Jobs, Interviews, Offers, and more.
The honest trade-off: you get speed to market and reduced maintenance burden, but you give up some control over the raw API interaction. If you need deep access to Lever-specific endpoints like requisition fields or audit events, a unified API might not cover every edge case. Truto addresses this with a Proxy API that gives you unmapped, direct access to any Lever endpoint when the unified model doesn't fit.
What Actually Ships This Quarter
If you're staring at a Jira epic called "Lever Integration," here's how to think about the effort realistically:
| Component | Build from scratch | With a unified API |
|---|---|---|
| OAuth flow + token refresh | 1–2 weeks | Configuration only |
| Core endpoints (opportunities, postings) | 1–2 weeks | Pre-built |
| Pagination + rate limit handling | 3–5 days | Handled automatically |
| Webhook ingestion + verification | 2–3 days | Pre-built |
| Second ATS (e.g., Greenhouse) | Another 3–4 weeks | Same interface |
| Ongoing maintenance per vendor | ~20% of initial build/year | Managed |
The real question is not whether you can build this. Of course you can. The question is whether building ATS connectors is the best use of your engineering team's time when the alternative is shipping the features that actually differentiate your product.
FAQ
- How does Lever API authentication work?
- Lever uses Basic Auth (API key as username, blank password) for internal workflows and OAuth 2.0 Authorization Code flow for customer-facing integrations. OAuth requires the 'audience' parameter in production. Access tokens expire after 1 hour, and refresh tokens rotate on each exchange.
- What is the Lever API rate limit?
- Lever allows a steady-state rate of 10 requests per second per API key with bursts up to 20 req/sec. Application POST requests are limited to just 2 per second. Exceeding these returns a 429 status code, and the limit may change without warning.
- How does Lever API pagination work?
- Lever uses cursor-based pagination with an opaque offset token. Each list response includes a 'next' field containing the token, which you pass as the 'offset' query parameter. You cannot construct offset tokens yourself, and there is no total count in the response.
- What is the difference between Candidates and Opportunities in Lever's API?
- The Candidates endpoints are deprecated. Opportunities are the primary object, representing a person's candidacy for a specific job. A single Contact (person) can have multiple Opportunities, each with at most one Application. Use '/opportunities' and track the 'contact' field as the unique person identifier.
- Does Lever support webhooks?
- Yes. Lever supports webhooks for events like candidateHired, candidateStageChange, and applicationCreated. Payloads are signed with HMAC-SHA256, with the signature embedded in the POST body rather than an HTTP header. Lever retries failed deliveries up to 5 times, and only HTTPS endpoints are supported.