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.
https://propertyroles.uk/api/v1The 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 id | Name | Primary domain | Niche |
|---|---|---|---|
propertyroles | PropertyRoles.uk | propertyroles.uk | property |
d365 | D365.careers | d365.careers | Dynamics 365 |
automationroles-ai | AutomationRoles.ai | automationroles.ai | AI automation |
propertyroles-com-au | PropertyRoles.com.au | propertyroles.com.au | property |
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.
Authorization: Bearer bk_live_YOUR_KEYA 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.
{
"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.
| Field | Type | Notes |
|---|---|---|
boardrequired | string | Board id or domain, e.g. "propertyroles". |
external_idrequired | string | Your own stable id (max 200). Required in the body for POST; taken from the URL for PUT/PATCH/DELETE. |
company.namerequired | string | Hiring company name (max 200). |
company.websiteoptional | string (url) | Company website, http(s). |
titlerequired | string | Job title (max 200). |
descriptionrequired | string | Full advert body, plain text or simple markup (max 20,000). |
apply_urlrequired | string (url) | Where applicants are sent. http(s), max 2048. |
locationrequired | string | Free-text location, resolved to a verified city/town. Pre-check with /locations/resolve. |
employment_typeoptional | enum | One of FULL_TIME, PART_TIME, CONTRACTOR, TEMPORARY, INTERN. |
categoriesoptional | string[] | Up to 10 category slugs (each max 80 chars). |
salary.min / salary.maxoptional | number | Non-negative; min must be ≤ max. |
salary.currencyoptional | string | ISO 4217, 3 letters, e.g. GBP. |
salary.periodoptional | enum | year, month, week, day or hour. |
expires_atoptional | string (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
/jobsStrict 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.
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
/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.
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
/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.
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
/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.
curl -X DELETE "https://propertyroles.uk/api/v1/jobs/silky-12345" \
-H "Authorization: Bearer bk_live_YOUR_KEY"Fetch a job
/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.
curl "https://propertyroles.uk/api/v1/jobs/silky-12345" \
-H "Authorization: Bearer bk_live_YOUR_KEY"List your jobs
/jobsPage 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.
curl "https://propertyroles.uk/api/v1/jobs?status=active&limit=20&offset=0" \
-H "Authorization: Bearer bk_live_YOUR_KEY"List boards
/boardsDiscover which boards this key may post to — use it to drive a board picker in your UI rather than hard-coding ids.
curl "https://propertyroles.uk/api/v1/boards" \
-H "Authorization: Bearer bk_live_YOUR_KEY"{
"data": [
{
"board": "propertyroles",
"name": "PropertyRoles.uk",
"domains": ["propertyroles.uk"],
"niche": "property"
}
],
"request_id": "req_5f3a…"
}Resolve a location
/locations/resolvePre-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.
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" }'{
"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": {
"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…"
}| HTTP | code | Meaning |
|---|---|---|
400 | malformed_json | Body is not valid JSON. |
401 | unauthorized | Missing or invalid API key. |
401 | key_revoked | The API key has been revoked. |
403 | board_forbidden | Key not entitled to this board. |
404 | board_not_found | Unknown board id or domain. |
404 | job_not_found | No advert with that external_id for your key. |
409 | job_already_exists | POST onto an existing external_id. |
413 | payload_too_large | Request body exceeds the size limit. |
415 | unsupported_media_type | Content-Type is not application/json. |
422 | validation_failed | One or more fields failed validation (see details). |
422 | location_unresolved | The location could not be matched. |
429 | rate_limited | Too many requests; see Retry-After. |
405 | method_not_allowed | Verb not supported on this path. |
500 | internal_error | Unexpected 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.
POST, PUT, PATCH, DELETE, /locations/resolve
60 / min · 2,000 / hour
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.
- 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. - 2On publish/edit,
PUT https://propertyroles.uk/api/v1/jobs/{silkyJobId}with the advert body. Treat200and201as success. - 3On close,
DELETEthe same path. It is idempotent, so retries and double-closes are harmless. - 4Surface
request_idanderror.detailsin 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.
