> Reverse Contact API documentation. This is the Markdown copy of https://docs.reversecontact.com/sdk/javascript
> Every page is listed at https://docs.reversecontact.com/llms.txt, every endpoint with its credit cost and status at https://docs.reversecontact.com/catalog.json

# JavaScript and TypeScript

> Call the Reverse Contact API from Node.js, Bun or Deno with a typed client generated from the OpenAPI spec, or with a small fetch helper.

- Last verified: 2026-10-15

## Introduction

There is no Reverse Contact package to install. The API is JSON over HTTPS, so the `fetch` built into Node.js 18 and later, Bun and Deno is all a call needs. For types and autocompletion on every request and response, generate a client from the OpenAPI spec this site publishes at `https://docs.reversecontact.com/openapi.json`, which is updated with each docs release.

Keep the API key on the server, in an environment variable such as `RC_API_KEY`. Never send it to a browser: see [Authentication](https://docs.reversecontact.com/authentication).

## Typed client from the OpenAPI spec

[openapi-typescript](https://openapi-ts.dev) turns the spec into TypeScript types, and `openapi-fetch` is a small `fetch` wrapper that checks every path, body and response against them. `openapi-typescript` needs TypeScript 5, hence `typescript@5`.

**Install**

```bash [npm]
npm install openapi-fetch
npm install --save-dev openapi-typescript typescript@5
npx openapi-typescript https://docs.reversecontact.com/openapi.json -o reversecontact.d.ts
```
```bash [Bun]
bun add openapi-fetch
bun add --dev openapi-typescript typescript@5
bunx openapi-typescript https://docs.reversecontact.com/openapi.json -o reversecontact.d.ts
```

Run the last command again whenever the [changelog](https://docs.reversecontact.com/changelog) announces a change to an endpoint you call.

**Enrich a person**

```typescript [TypeScript]
import createClient from 'openapi-fetch'
import type { paths } from './reversecontact'

const rc = createClient<paths>({
  baseUrl: 'https://api.reversecontact.com',
  headers: { Authorization: `Bearer ${process.env.RC_API_KEY}` }
})

// Enrich a person from a work email: 2 credits when found, a 404 is free.
const { data, error, response } = await rc.POST('/v2/enrich/persons', {
  body: { email: 'jane.doe@acme.com' }
})

if (data) {
  const person = data.data?.person
  console.log(person?.firstName, person?.lastName, person?.currentPositionTitle)
  console.log('Credits used:', data.quotas.creditsConsumed)
} else if (response.status === 404) {
  console.log('No confident match: nothing was charged')
} else {
  throw new Error(`${response.status} ${error?.error.code}: ${error?.error.message}`)
}
```

The paths keep their `/v2` prefix (`/v2/enrich/persons`), as in the spec. On Enrich, a `404` is not an error to retry: it means no profile matched with confidence, and it costs nothing.

## Async jobs

The live fetch endpoints, [Find Email](https://docs.reversecontact.com/endpoints/contact-email) and [Find Phone](https://docs.reversecontact.com/endpoints/contact-phone) answer with a job. Poll it until it succeeds or fails: polling is free and does not count toward your rate limit. Give the loop a maximum duration, and stop on a poll that answers an error: `404 WEBHOOK_EVENT_NOT_FOUND` for an unknown job, `410 WEBHOOK_EVENT_EXPIRED` for a result older than 72 hours.

**Live fetch and polling**

```typescript [TypeScript]
const { data: job, error: jobError } = await rc.POST('/v2/fetch/persons/live', {
  body: { url: 'https://social.com/in/janedoe' }
})
const webhookId = job?.data?.webhookId
if (!webhookId) throw new Error(jobError?.error.message ?? 'No job created')

// Results usually arrive within seconds, but can take tens of minutes.
const deadline = Date.now() + 30 * 60 * 1000

for (;;) {
  if (Date.now() > deadline) throw new Error(`Job ${webhookId} still running: poll it again later`)
  await new Promise((resolve) => setTimeout(resolve, 5000))
  const { data: poll, error: pollError, response } = await rc.GET('/v2/webhooks/{webhookId}', {
    params: { path: { webhookId } }
  })
  // A 5xx is temporary: poll again. 404 and 410 mean there is nothing left to wait for.
  if (!poll) {
    if (response.status >= 500) continue
    throw new Error(`${response.status} ${pollError?.error.code}: ${pollError?.error.message}`)
  }
  if (poll.data?.status === 'succeeded') {
    console.log(poll.data.result)
    break
  }
  if (poll.data?.status === 'errored') throw new Error(poll.data.errorCode ?? 'Job errored')
}
```

Every 5 to 10 seconds suits a background job. See [Polling](https://docs.reversecontact.com/guides/polling) for the intervals that fit other cases, and for how long a result stays available.

## Without code generation

When you would rather not generate types, this helper is enough. It follows the retry rule of [Error handling & retries](https://docs.reversecontact.com/guides/errors-retries): on a `429`, wait the number of seconds in `error.details.retryAfter`, then retry; on a `500`, `502`, `503` or `504`, retry with exponential backoff, a few times at most. It reads the status before the body, because a `502` or `503` from the network edge may not be JSON. A `429` with the code `DAILY_LIMIT_EXCEEDED` is the daily limit: it is not retried, since it does not clear within minutes.

**fetch helper**

```typescript [TypeScript]
const API = 'https://api.reversecontact.com/v2'
const BACKOFF = new Set([500, 502, 503, 504])
const MAX_ATTEMPTS = 5
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

/** One call to the API. 429: waits error.details.retryAfter. 500, 502, 503, 504: backs off. */
export async function rc(path: string, body?: unknown) {
  for (let attempt = 1; ; attempt++) {
    const response = await fetch(`${API}${path}`, {
      method: body === undefined ? 'GET' : 'POST',
      headers: {
        Authorization: `Bearer ${process.env.RC_API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: body === undefined ? undefined : JSON.stringify(body)
    })

    // The status first: a 502 or 503 from the network edge may not be JSON.
    if (BACKOFF.has(response.status) && attempt < MAX_ATTEMPTS) {
      await sleep(Math.min(1000 * 2 ** attempt, 15000))
      continue
    }
    const json = await response.json().catch(() => ({
      success: false,
      error: { code: `HTTP_${response.status}`, message: 'The response is not JSON' }
    }))
    if (response.status === 429 && json.error?.code !== 'DAILY_LIMIT_EXCEEDED' && attempt < MAX_ATTEMPTS) {
      await sleep((json.error?.details?.retryAfter ?? 60) * 1000)
      continue
    }
    return { status: response.status, ...json }
  }
}

const result = await rc('/enrich/persons', { email: 'jane.doe@acme.com' })
if (result.success) console.log(result.data.person.currentPositionTitle)
else if (result.status === 404) console.log('No confident match: nothing was charged')
else console.error(result.status, result.error.code, result.error.message)
```

Here the paths start after `/v2`. What a `404` means depends on the endpoint: on Enrich, no confident match; on a cached fetch, a profile that is not in the cache; on `/v2/webhooks`, an unknown job. Every error other than the retried ones needs a change to the request or the account before it can succeed.

## FAQ

### Is there an official Reverse Contact npm package?

No. The Reverse Contact API is JSON over HTTPS, so `fetch` is enough, and a typed client generated from `https://docs.reversecontact.com/openapi.json` with `openapi-typescript` and `openapi-fetch` gives types for every endpoint.

### Can I call the Reverse Contact API from the browser?

Not with your API key: anyone can read a key shipped in front-end code. Call the API from your server, keep the key in an environment variable such as `RC_API_KEY`, and send the browser only what it needs.

### Which Reverse Contact API errors should my code retry?

On a `429`, wait the number of seconds in `error.details.retryAfter`, then retry, except for `DAILY_LIMIT_EXCEEDED`, the daily limit. Retry `500`, `502`, `503` and `504` with exponential backoff, a few times at most. Every other error needs a change to the request or the account first.
