Developersv1

Job Posting API

Post, update and expire job adverts on PropertyRoles.uk and every Brainy board over a simple, idempotent REST API. Built for products like Silky to publish in real time.

Overview

The Brainy Partner Job API lets an external product post, edit and expire job adverts on any Brainy-powered board. You address each advert by your own external_id, so the integration is stateless on your side — there is no Brainy job id to store. The PUT create-or-replace verb is idempotent and is the recommended path: re-sending the same advert is always safe.

  • REST over HTTPS, JSON in and JSON out.
  • Bearer API key auth, scoped to the boards you are allowed to post to.
  • Idempotent writes keyed on your own external_id.
  • Versioned under /api/v1 — new fields are additive, never breaking.

Base URL & boards

The API is served from every board domain. Use the board you are posting to as the host — the path is always /api/v1. The board is also named in the request body, so any board host accepts adverts for any board your key is entitled to.

Base URL
https://propertyroles.uk/api/v1

The board network

Pass the board id or any of its domains as the board field. Call GET /boards at runtime to discover exactly which boards your key may post to.

Board idNamePrimary domainNiche
propertyrolesPropertyRoles.ukpropertyroles.ukproperty
d365D365.careersd365.careersDynamics 365
automationroles-aiAutomationRoles.aiautomationroles.aiAI automation
propertyroles-com-auPropertyRoles.com.aupropertyroles.com.auproperty

Authentication

Every request carries a bearer API key in the Authorization header. Keys are minted in the Brainy admin under API clients and look like bk_live_…. Treat a key like a password: it is shown once at creation, store it as a secret, and never ship it in client-side code.

Header
Authorization: Bearer bk_live_YOUR_KEY

A key is bound to a client (e.g. Silky) and, optionally, to a specific allow-list of boards. Posting to a board outside that list returns 403 board_forbidden. A revoked key returns 401 key_revoked. Requests are server-to-server only.

Quickstart

Publish your first advert with a single idempotent request. Pick a stable id of your own (here silky-12345) and PUT it. Send the same request again any time the advert changes — Brainy creates it the first time and replaces it thereafter.

curl -X PUT "https://propertyroles.uk/api/v1/jobs/silky-12345" \
  -H "Authorization: Bearer bk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "board": "propertyroles",
  "company": { "name": "Acme Property Group" },
  "title": "Senior Property Manager",
  "description": "We are hiring an experienced property manager to run a portfolio of commercial sites across central London.",
  "apply_url": "https://careers.acme.com/jobs/123",
  "location": "London, United Kingdom",
  "employment_type": "FULL_TIME",
  "categories": ["property-management", "commercial"],
  "salary": { "min": 45000, "max": 55000, "currency": "GBP", "period": "year" },
  "expires_at": "2026-09-30T23:59:59Z"
}'

What you get back

A 201 Created (or 200 OK on a replace) with the canonical advert and the live URL it now occupies on the board.

200 / 201 response
{
  "data": {
    "external_id": "silky-12345",
    "board": "propertyroles",
    "status": "active",
    "title": "Senior Property Manager",
    "company": "Acme Property Group",
    "location": "London, United Kingdom",
    "apply_url": "https://careers.acme.com/jobs/123",
    "employment_type": "FULL_TIME",
    "seniority": "senior",
    "categories": ["property-management", "commercial"],
    "salary": { "min": 45000, "max": 55000, "currency": "GBP", "period": "year" },
    "url": "https://propertyroles.uk/jobs/senior-property-manager-acme-london",
    "posted_at": "2026-06-23T14:30:00Z",
    "expires_at": "2026-09-30T23:59:59Z",
    "created_at": "2026-06-23T14:30:00Z",
    "updated_at": "2026-06-23T14:30:00Z"
  },
  "request_id": "req_5f3a…"
}

The job object

The same body shape is used by POST and PUT. Unknown fields are rejected (strict schema), so typos surface as a 422 rather than being silently dropped.

FieldTypeNotes
boardrequiredstringBoard id or domain, e.g. "propertyroles".
external_idrequiredstringYour own stable id (max 200). Required in the body for POST; taken from the URL for PUT/PATCH/DELETE.
company.namerequiredstringHiring company name (max 200).
company.websiteoptionalstring (url)Company website, http(s).
titlerequiredstringJob title (max 200).
descriptionrequiredstringFull advert body, plain text or simple markup (max 20,000).
apply_urlrequiredstring (url)Where applicants are sent. http(s), max 2048.
locationrequiredstringFree-text location, resolved to a verified city/town. Pre-check with /locations/resolve.
employment_typeoptionalenumOne of FULL_TIME, PART_TIME, CONTRACTOR, TEMPORARY, INTERN.
categoriesoptionalstring[]Up to 10 category slugs (each max 80 chars).
salary.min / salary.maxoptionalnumberNon-negative; min must be ≤ max.
salary.currencyoptionalstringISO 4217, 3 letters, e.g. GBP.
salary.periodoptionalenumyear, month, week, day or hour.
expires_atoptionalstring (ISO-8601)Auto-expiry time. Must be in the future and within 180 days.

Endpoints

All paths are relative to https://propertyroles.uk/api/v1. Writes are rate-limited; see rate limits.

Create a job

POST/jobs

Strict create. Fails with 409 job_already_exists if an advert with that external_id already exists for your key. Use this when you want to be told about duplicates; otherwise prefer PUT.

POST /jobs
curl -X POST "https://propertyroles.uk/api/v1/jobs" \
  -H "Authorization: Bearer bk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "board": "propertyroles",
    "external_id": "silky-12345",
    "company": { "name": "Acme Property Group" },
    "title": "Senior Property Manager",
    "description": "We are hiring…",
    "apply_url": "https://careers.acme.com/jobs/123",
    "location": "London, United Kingdom"
  }'

Create or replace

PUT/jobs/{external_id}

Idempotent upsert — the recommended integration path. The external_id comes from the URL; if you also send it in the body it must match. Returns 201 on first create, 200 on replace.

PUT /jobs/{external_id}
curl -X PUT "https://propertyroles.uk/api/v1/jobs/silky-12345" \
  -H "Authorization: Bearer bk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "board": "propertyroles",
  "company": { "name": "Acme Property Group" },
  "title": "Senior Property Manager",
  "description": "We are hiring an experienced property manager to run a portfolio of commercial sites across central London.",
  "apply_url": "https://careers.acme.com/jobs/123",
  "location": "London, United Kingdom",
  "employment_type": "FULL_TIME",
  "categories": ["property-management", "commercial"],
  "salary": { "min": 45000, "max": 55000, "currency": "GBP", "period": "year" },
  "expires_at": "2026-09-30T23:59:59Z"
}'

Update a job

PATCH/jobs/{external_id}

Partial update of content only. Send just the fields that changed (at least one is required). Identity — board and external_id — is immutable; to move an advert to a different board, expire it and create a new one.

PATCH /jobs/{external_id}
curl -X PATCH "https://propertyroles.uk/api/v1/jobs/silky-12345" \
  -H "Authorization: Bearer bk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "salary": { "min": 50000, "max": 60000, "currency": "GBP", "period": "year" } }'

Expire a job

DELETE/jobs/{external_id}

Soft-expire the advert. Idempotent and non-destructive — the record is retained for audit and can be brought back with another PUT. The advert stops appearing on the board immediately.

DELETE /jobs/{external_id}
curl -X DELETE "https://propertyroles.uk/api/v1/jobs/silky-12345" \
  -H "Authorization: Bearer bk_live_YOUR_KEY"

Fetch a job

GET/jobs/{external_id}

Read back a single advert by your external_id. Returns 404 job_not_found if it does not exist for your key.

GET /jobs/{external_id}
curl "https://propertyroles.uk/api/v1/jobs/silky-12345" \
  -H "Authorization: Bearer bk_live_YOUR_KEY"

List your jobs

GET/jobs

Page through the adverts your key owns. Query parameters: board (filter to one board), status (active or expired), limit (1–100, default 50) and offset. The response carries a meta block with total, limit and offset.

GET /jobs?status=active&limit=20
curl "https://propertyroles.uk/api/v1/jobs?status=active&limit=20&offset=0" \
  -H "Authorization: Bearer bk_live_YOUR_KEY"

List boards

GET/boards

Discover which boards this key may post to — use it to drive a board picker in your UI rather than hard-coding ids.

GET /boards
curl "https://propertyroles.uk/api/v1/boards" \
  -H "Authorization: Bearer bk_live_YOUR_KEY"
200 response
{
  "data": [
    {
      "board": "propertyroles",
      "name": "PropertyRoles.uk",
      "domains": ["propertyroles.uk"],
      "niche": "property"
    }
  ],
  "request_id": "req_5f3a…"
}

Resolve a location

POST/locations/resolve

Pre-validate a free-text location before posting, so you never get a 422 location_unresolved mid-sync. Pass an optional boardto scope resolution to that board’s allowed countries.

POST /locations/resolve
curl -X POST "https://propertyroles.uk/api/v1/locations/resolve" \
  -H "Authorization: Bearer bk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "board": "propertyroles", "location": "Manchester" }'
Resolved
{
  "data": {
    "resolved": true,
    "location_id": "loc_manchester_gb",
    "label": "Manchester, England",
    "slug": "manchester",
    "country_code": "GB"
  },
  "request_id": "req_5f3a…"
}

Errors

Errors use a consistent envelope. The request_id is also returned as the X-Request-Id header — quote it when reporting an issue. Validation failures list each offending field in details.

Error envelope
{
  "error": {
    "code": "validation_failed",
    "message": "The request body failed validation.",
    "details": [
      { "field": "salary.min", "message": "must be >= 0" },
      { "field": "apply_url", "message": "must be a valid http(s) URL" }
    ]
  },
  "request_id": "req_5f3a…"
}
HTTPcodeMeaning
400malformed_jsonBody is not valid JSON.
401unauthorizedMissing or invalid API key.
401key_revokedThe API key has been revoked.
403board_forbiddenKey not entitled to this board.
404board_not_foundUnknown board id or domain.
404job_not_foundNo advert with that external_id for your key.
409job_already_existsPOST onto an existing external_id.
413payload_too_largeRequest body exceeds the size limit.
415unsupported_media_typeContent-Type is not application/json.
422validation_failedOne or more fields failed validation (see details).
422location_unresolvedThe location could not be matched.
429rate_limitedToo many requests; see Retry-After.
405method_not_allowedVerb not supported on this path.
500internal_errorUnexpected server error — safe to retry.

Rate limits

Limits are per API key. Exceeding them returns 429 rate_limited with a Retry-After header (seconds) — back off and retry.

Writes

POST, PUT, PATCH, DELETE, /locations/resolve

60 / min · 2,000 / hour

Reads

All GET requests

300 / min

Integrating Silky

Silky already owns each advert it manages, so the integration is simple: mirror Silky’s own id into external_id and PUT on every publish or edit. Expire on close. No Brainy ids to persist, and re-sends are always safe.

  1. 1Create an API client in Brainy admin → API clients, scoped to the boards Silky should reach. Store the bk_live_… key as a Silky secret.
  2. 2On publish/edit, PUT https://propertyroles.uk/api/v1/jobs/{silkyJobId} with the advert body. Treat 200 and 201 as success.
  3. 3On close, DELETE the same path. It is idempotent, so retries and double-closes are harmless.
  4. 4Surface request_id and error.details in Silky logs to debug rejected adverts quickly.

Tip: call POST /locations/resolve when a recruiter types a location in Silky, and store the resolved label. That guarantees the later PUT never fails on an unrecognised place.