# Migrate from Techmap

> How do I translate my Techmap queries and fields to BetterJobs?

Source: https://docs.betterjobs.cc/guides/migrate-from-techmap/

Techmap (jobdatafeeds.com) sells high-volume job feeds: an API priced per thousand jobs, daily country files and a `/count` endpoint to estimate cost. BetterJobs replaces the feed-and-dedup work with one search that merges Techmap and up to six other sources into canonical jobs.

Techmap is one of the six providers behind BetterJobs, so its postings can still reach you through the waterfall.

### [Techmap](https://docs.betterjobs.cc/providers/techmap.md)

`techmap`

High-volume job feeds from ATSs, job boards and public employment offices (jobdatafeeds.com).

- Sources

  200+ sources incl. 120 ATSs and 28 public employment offices

- New postings

  About 8M per month

- History

  451M+ postings since 2020

- Countries

  Claims 250 countries (about 125 with more than 100 jobs per month)

- Feeds

  Daily country feeds via AWS Data Exchange

- API

  $1 per 1k jobs; /count endpoint estimates cost

Growth includes 2 partner providers; Pro and above include all six. GET /v1/providers shows what your plan enables.

Facts per Techmap (jobdatafeeds.com).

[Provider docs ↗](https://jobdatafeeds.com)

## What changes, in one table

| Topic                   | Techmap (per Techmap)                               | BetterJobs                                                                  |
| ----------------------- | --------------------------------------------------- | --------------------------------------------------------------------------- |
| Delivery                | API, plus daily country feeds via AWS Data Exchange | API: sync search (100 jobs per page) and async search (up to 10,000 jobs)   |
| Estimate before you pay | `/count` endpoint                                   | `dry_run: true`, free                                                       |
| Price                   | $1 per 1,000 jobs via the API                       | 1 credit per unique job; dollar value depends on your plan (below)          |
| Duplicates              | `isDuplicate` flag on records                       | Merged into one canonical job; every source in `sources[]`; duplicates free |
| Formats                 | json, csv, rss, parquet                             | JSON API. CSV on Growth and above. No parquet or RSS in v1 preview          |
| Raw posting markup      | `jsonLD` (schema.org `JobPosting`)                  | Normalized fields only. No JSON-LD passthrough                              |
| History                 | 451M+ postings since 2020                           | `posted_within_days` up to 365, `include_closed: true` for closed jobs      |

> Honest price check
>
> Per Techmap, its API costs $1 per 1,000 jobs. A BetterJobs credit costs more: Growth $4.90, Pro $3.32, Scale $2.40 per 1,000 credits. You pay the difference for merge and dedup across every source your plan enables (Growth: the BetterJobs index plus 2 partner providers; Pro and above: all six), cross-source `p_real`, lifecycle events and one bill. If you only need Techmap’s raw feed at volume, Techmap direct is cheaper. On Scale you can bring your own provider keys.

## Translate a request

Before: pull a feed or API page, estimate with `/count`, drop flagged duplicates, normalize. `techmap_count` and `techmap_fetch` stand for your existing wrappers.

```python
# Before (Techmap): estimate, fetch, then dedup and normalize yourself
cost = techmap_count(query)                     # /count estimates cost, per Techmap
rows = techmap_fetch(query)                     # $1 per 1k jobs, per Techmap
rows = [r for r in rows if not r["isDuplicate"]]
jobs = [normalize(r["jsonLD"]) for r in rows]   # your own mapping from schema.org JobPosting
```

After: estimate free, then fetch merged canonical jobs with a credit cap.

**curl**

```bash
# Free estimate (replaces /count)
curl https://api.betterjobs.cc/v1/jobs/search \
  -H "Authorization: Bearer $BETTERJOBS_API_KEY" \
  -H "BetterJobs-Version: 2026-10-01" \
  -H "Content-Type: application/json" \
  -d '{
    "filters": {
      "title_or": ["Warehouse Manager", "Logistics Manager"],
      "country_code_or": ["DE"],
      "employment_type_or": ["full_time"],
      "posted_within_days": 1
    },
    "waterfall": { "strategy": "max_coverage" },
    "limit": 100,
    "dry_run": true
  }'


# Fetch merged jobs (replaces fetch + isDuplicate filter + normalize)
curl https://api.betterjobs.cc/v1/jobs/search \
  -H "Authorization: Bearer $BETTERJOBS_API_KEY" \
  -H "BetterJobs-Version: 2026-10-01" \
  -H "Content-Type: application/json" \
  -d '{
    "filters": {
      "title_or": ["Warehouse Manager", "Logistics Manager"],
      "country_code_or": ["DE"],
      "employment_type_or": ["full_time"],
      "posted_within_days": 1
    },
    "waterfall": { "strategy": "max_coverage", "max_credits": 100 },
    "limit": 100
  }'
```

**Python**

```python
import os


import requests


API = "https://api.betterjobs.cc/v1"
HEADERS = {
    "Authorization": f"Bearer {os.environ['BETTERJOBS_API_KEY']}",
    "BetterJobs-Version": "2026-10-01",
}
BODY = {
    "filters": {
        "title_or": ["Warehouse Manager", "Logistics Manager"],
        "country_code_or": ["DE"],
        "employment_type_or": ["full_time"],
        "posted_within_days": 1,
    },
    "waterfall": {"strategy": "max_coverage"},
    "limit": 100,
}


estimate = requests.post(f"{API}/jobs/search", headers=HEADERS, json={**BODY, "dry_run": True}, timeout=30)
estimate.raise_for_status()
print(estimate.json()["estimate"])  # free, replaces /count


body = {**BODY, "waterfall": {**BODY["waterfall"], "max_credits": 100}}
resp = requests.post(f"{API}/jobs/search", headers=HEADERS, json=body, timeout=30)
resp.raise_for_status()
jobs = resp.json()["data"]  # merged, deduplicated, normalized
```

**TypeScript**

```ts
const API = 'https://api.betterjobs.cc/v1';
const headers = {
  Authorization: `Bearer ${process.env.BETTERJOBS_API_KEY}`,
  'BetterJobs-Version': '2026-10-01',
  'Content-Type': 'application/json',
};
const body = {
  filters: {
    title_or: ['Warehouse Manager', 'Logistics Manager'],
    country_code_or: ['DE'],
    employment_type_or: ['full_time'],
    posted_within_days: 1,
  },
  waterfall: { strategy: 'max_coverage' },
  limit: 100,
};


async function search(payload: object) {
  const res = await fetch(`${API}/jobs/search`, { method: 'POST', headers, body: JSON.stringify(payload) });
  if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
  return res.json();
}


console.log((await search({ ...body, dry_run: true })).estimate); // free, replaces /count
const { data: jobs } = await search({ ...body, waterfall: { ...body.waterfall, max_credits: 100 } });
```

### Replacing the daily country feed

A daily country file becomes a daily search per country with one day of overlap:

```json
{
  "filters": { "country_code_or": ["DE"], "posted_within_days": 2 },
  "waterfall": { "strategy": "max_coverage", "max_credits": 10000 },
  "limit": 10000
}
```

Send it to `POST /v1/searches` (async, up to 10,000 jobs), read the pages for free and upsert on `id`. Jobs from the overlap day that you already paid for are free. Add a search watch with `job.closed` to mark expired jobs, also free. The full recipe is on [Sync patterns](https://docs.betterjobs.cc/guides/sync-patterns.md).

> Size it before you schedule it
>
> A whole country per day can exceed 10,000 jobs. Send the same `filters` and `waterfall` to `POST /v1/jobs/search` with `"limit": 100` and `"dry_run": true` first (async searches have no `dry_run`). `expected_unique_jobs_range` counts every matching job, not one page. If its `max` is above 10,000, split by `job_family_or` or by title groups.

## Map the fields

Field names in Techmap records and their BetterJobs equivalents. Rows show only fields with a confident one-to-one match.

| BetterJobs        | Techmap        |
| ----------------- | -------------- |
| `employment_type` | `contractType` |

Other Techmap fields, per its reference, and where that information lives in BetterJobs:

| Techmap field | BetterJobs               | Note                                                                                                            |
| ------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------- |
| `isDuplicate` | none needed              | Duplicates are merged. `sources[]` lists every source that saw the job                                          |
| `jsonLD`      | the canonical job fields | Normalized, not passed through. See the [field dictionary](https://docs.betterjobs.cc/data/field-dictionary.md) |
| `dateActive`  | —                        | No confident equivalent. Compare with `first_seen_at`, `last_seen_at`, `last_verified_at`                       |
| `workPlace`   | —                        | No confident equivalent. Compare with `location.*` and `location.remote`                                        |

## Billing differences

1. **Per unique job, not per row.** Techmap prices by jobs delivered ($1 per 1,000 via the API, per Techmap). BetterJobs charges 1 credit per unique canonical job. Records from other sources that describe the same opening are merged for free.

2. **Re-reads are free.** A job you already paid for returns free in later searches. Overlapping daily windows cost nothing extra.

3. **Estimates are free in both.** `/count` on Techmap, `dry_run` on BetterJobs.

4. **Proof of charge.** `GET /v1/billing/ledger?job_id=...` lists each charge and each free re-read.

Techmap is a partner provider: Growth includes two partner providers, Pro and above include all six. `GET /v1/providers` shows whether your plan enables `techmap`. Plans are on [Credits and billing](https://docs.betterjobs.cc/concepts/credits-and-billing.md).

## What changes in your code

1. **Replace file ingestion with API pages.** Async search plus cursor paging replaces downloading daily files.

2. **Delete the dedup step.** No `isDuplicate` filter. Key rows on the canonical `id`.

3. **Delete your JSON-LD mapping.** Read normalized fields such as `title`, `location`, `employment_type`, `salary` directly.

4. **Map `contractType` to `employment_type`.** Values are `full_time`, `part_time`, `contract`, `internship`, `temporary` or `null`. See [Taxonomies](https://docs.betterjobs.cc/data/taxonomies.md).

5. **Handle closures with events.** Use `job.closed` from a watch instead of diffing daily files. See [Detect hiring changes](https://docs.betterjobs.cc/guides/detect-hiring-changes.md).

## Pitfalls

> Country count is not coverage
>
> Per Techmap, it claims 250 countries, about 125 with more than 100 jobs per month. BetterJobs coverage per country is not published during the preview. Measure on your own queries with `metadata.providers.contributions` and `metadata.field_coverage`.

- **`posted_within_days` counts from `first_seen_at`.** It is the earliest sighting across all sources.
- **No parquet in v1 preview.** Write the JSON pages to parquet yourself if your warehouse needs it.
- **`null` is unknown.** `employment_type: null` means no source said, not “other”.

## Next

- [Techmap provider page](https://docs.betterjobs.cc/providers/techmap.md)
- [Sync patterns](https://docs.betterjobs.cc/guides/sync-patterns.md) and [Async searches](https://docs.betterjobs.cc/platform/async-searches.md)
- [Choose a strategy](https://docs.betterjobs.cc/guides/choose-a-strategy.md)
