---
title: How to Integrate with the Lever API (2026 Engineering Guide)
slug: how-to-integrate-with-the-lever-api-2026-engineering-guide
date: 2026-03-30
author: Sidharth Verma
categories: [Guides, Engineering, By Example]
excerpt: "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."
tldr: "Lever's API requires OAuth with 1-hour tokens, offset-token pagination, 10 req/sec rate limits (2 req/sec for POSTs), and an Opportunity-centric data model. A unified API abstracts these quirks across ATS platforms."
canonical: https://truto.one/blog/how-to-integrate-with-the-lever-api-2026-engineering-guide/
---

# How to Integrate with the Lever API (2026 Engineering Guide)


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](https://truto.one/blog/building-native-hris-integrations-without-draining-engineering-in-2026/) or [CRM integrations](https://truto.one/blog/building-native-crm-integrations-without-draining-engineering-in-2026/), 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](https://truto.one/blog/how-to-integrate-multiple-ats-platforms-greenhouse-lever-workable/), 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](https://truto.one/blog/building-native-hris-integrations-without-draining-engineering-in-2026/) 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.

```bash
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:

1. **Redirect the user** to `https://auth.lever.co/authorize` with your `client_id`, `redirect_uri`, `response_type=code`, a `state` token for CSRF protection, the **required** `audience` parameter (`https://api.lever.co/v1/`), and your requested scopes.
2. **Exchange the authorization code** for tokens at `https://auth.lever.co/oauth/token`.
3. **Use the access token** as a Bearer token in the `Authorization` header.

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.

```python
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.

> [!WARNING]
> **The Refresh Token Race Condition**<br>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](https://truto.one/blog/oauth-at-scale-the-architecture-of-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**.

```mermaid
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.

```json
{
  "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:

```python
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_results
```

The 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 `hasNext` returns `false`. 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](https://truto.one/blog/how-do-unified-apis-handle-pagination-differences-across-rest-apis/), 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:

```python
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](https://truto.one/blog/best-practices-for-handling-api-rate-limits-and-retries-across-multiple-third-party-apis/).

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:

1. Concatenate the `token` and `triggeredAt` values from the payload body.
2. HMAC-SHA256 the result using your webhook signature token as the key.
3. Compare the hex digest to the `signature` field.

```javascript
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;
}
```

> [!WARNING]
> **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](https://truto.one/blog/how-to-integrate-with-the-greenhouse-api-a-guide-for-b2b-saas/), 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.

> Need to ship a Lever integration this quarter — without building pagination, OAuth refresh, and rate limit handling from scratch? Truto's unified ATS API handles Lever, Greenhouse, Workable, Ashby, and 20+ other platforms through a single interface. Get in touch.
>
> [Talk to us](https://cal.com/truto/partner-with-truto)
