# Rival Jobs API

> What Rival (formerly SilkRoad) boards publish, how their HTML is read, and what ?source=rival returns today.

Source: https://hyperjobs.io/ats/rival

Rival is the recruiting platform formerly known as SilkRoad, and like several
renames in this pool the plumbing kept the old name: the boards are still served
from `jobs.silkroad.com/<tenant>/Careers`. Its customers are mid-size US
employers, in community services, healthcare and regional operations, with small
boards and low posting volume per company.

## What a Rival board looks like

Two GETs, no auth, HTML at both levels — there's no JSON API and no feed:

```bash
# List — 10 per page, fixed
curl "https://jobs.silkroad.com/<tenant>/Careers?page=1"

# Detail — the body lives here
curl "https://jobs.silkroad.com/<tenant>/Careers/jobs/<id>"
```

The board token is the tenant path segment, and it's case-insensitive — the server
canonicalises it, so two casings of one tenant are one board and have to be folded
to stay that way.

## What the adapter reads, and what it has to work around

**There is no JSON-LD, and the way there isn't matters.** The pages carry zero
`ld+json` blocks. What makes that dangerous rather than merely inconvenient is
that a generic JobPosting extractor doesn't return null here. It returns
`{description: ""}`. An adapter that trusts it stores hundreds of **empty
postings at HTTP 200**, each one looking like a successfully scraped job with a
blank body. The body is in the HTML; it has to be read from the DOM.

**Pagination is real, which is rare in this pool.** `?page=N` genuinely works —
pages 1, 2 and 3 return ten ids each with zero overlap. `PageSize` and `pageSize`
are ignored; the page size is always 10.

**But the pager UI under-reports.** It only ever renders links for pages 1 and 2,
even when page 3 has jobs on it. Derive the page count from the pager and you
truncate every board bigger than twenty postings, silently. The only correct
termination is crawling until a page comes back empty.

**The list's location beats the detail's.** The board row carries a clean,
geocodable `Jewett City, CT`. The detail carries the street address,
`1 Sylvandale Road, Jewett City, Connecticut`. The row is the better field, which
is the opposite of the usual arrangement and worth knowing before you reach for
the detail out of habit.

**Detail blocks are tenant-configurable.** Every field is optional *by tenant*
rather than by job, so a board that ships no category ships none anywhere, and a
sample of one board tells you nothing about the next.

**No posting date exists anywhere** on either level, checked. And
`DisplayMinSalary` is a bare amount with no period: `$70,000.00` (annual) and
`$20.00` (hourly) are indistinguishable, so it stays text rather than becoming a
wrong number.

Verified against a live board at 26 of 26 descriptions.

## Or use ours

Two GETs and no auth makes Rival look like a board you could read yourself. The
parts that cost you are the ones that fail quietly at HTTP 200: the extractor
that hands back a blank body, the pager that stops advertising at page two, the
tenant casing that splits one board in half.

You get the description read from the DOM rather than from JSON-LD that isn't
there, pages crawled until one comes back empty rather than counted off the
pager, the board row's geocodable location preferred over the detail's street
address, the tenant token folded so two casings stay one board, and
`DisplayMinSalary` left as text rather than guessed into a wrong number. No
posting date, because the platform doesn't publish one anywhere.

Then the usual: dedup and the [company join](/docs/objects/company), normalised
onto the same [schema](/docs/objects/job) as every other source.

Query it with `?source=rival`. [`/v1/meta`](/docs/api-reference/meta) is the live
vocabulary and the live counts — one cheap request, never stale, and the thing to
read rather than hardcode. An unknown `source` value isn't a `400`; it simply
matches nothing.

[Quickstart](/docs/quickstart) · [All platforms](/ats)
