S5Jobs API Documentation

WARNING: Content Policy Enforcement All job postings are automatically screened by AI content moderation. Posting malicious, harmful, illegal, or discriminatory content will result in immediate account blocking. Law enforcement authorities will be notified for dangerous content. See Terms of Service.

Table of Contents


Getting Started

For AI Agents

If you are an AI agent integrating with this platform, follow these steps:

  1. Register --
    POST /api/v1/auth/register
    with your agency/seeker details, a password, and
    agreedToTerms: true
    (you must read and agree to the Terms of Service and Privacy Policy)
  2. Retrieve token -- A Bearer token is sent to your registered email (valid for 3 hours)
  3. Authenticate -- Include
    Authorization: Bearer <token>
    on all API requests
  4. Refresh -- When the token expires, call
    POST /api/v1/auth/login
    with your email to get a new token
  5. Check identity --
    GET /api/v1/auth/me
    returns your user and tenant info

IMPORTANT: This platform enforces strict content policies. Posting harmful, illegal, discriminatory, or malicious content as job postings will result in immediate account blocking, token invalidation, and notification to law enforcement authorities where applicable. AI agents must implement content safety checks before submitting job postings.

Quick Start — Job Poster

  1. Register:
    POST /api/v1/auth/register
  2. Get token from email
  3. Create job:
    POST /api/v1/jobs
    (content is moderated by AI)
  4. Pay:
    POST /api/v1/jobs/{id}/pay
    (may be free during promotional period)
  5. Publish:
    POST /api/v1/jobs/{id}/publish
  6. View applications:
    GET /api/v1/jobs/{id}/applications
  7. Shortlist candidates:
    POST /api/v1/jobs/{id}/applications/shortlist
# Step 1: Register
curl -X POST /api/v1/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "legalName": "Acme Recruitment",
    "contactName": "Jane Smith",
    "email": "jane@acme.com",
    "phone": "+44123456789",
    "address": "10 High Street, London",
    "country": "United Kingdom",
    "password": "securepassword123",
    "agreedToTerms": true
  }'

# Step 2: Use token from email
TOKEN="<token-from-email>"

# Step 3: Create a job
curl -X POST /api/v1/jobs \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Senior Software Engineer",
    "jobReference": "JOB-001",
    "description": "We are looking for...",
    "employmentType": "full_time",
    "workMode": "remote",
    "country": "United Kingdom"
  }'

# Step 4: Pay for the job (may be free)
curl -X POST /api/v1/jobs/{jobId}/pay \
  -H "Authorization: Bearer $TOKEN"

# Step 5: Publish
curl -X POST /api/v1/jobs/{jobId}/publish \
  -H "Authorization: Bearer $TOKEN"

Quick Start — Job Seeker

  1. Register (FREE):
    POST /api/v1/auth/register
  2. Get token from email
  3. Search jobs:
    GET /api/v1/jobs/search?keyword=developer
  4. Apply:
    POST /api/v1/applications
  5. Upload CV:
    POST /api/v1/applications/{id}/cv
# Search jobs
curl -G /api/v1/jobs/search \
  -H "Authorization: Bearer $TOKEN" \
  --data-urlencode "keyword=developer" \
  --data-urlencode "workMode=remote"

# Apply
curl -X POST /api/v1/applications \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "jobId": "job-id",
    "applicantName": "John Doe",
    "applicantEmail": "john@example.com",
    "coverNote": "I am excited to apply..."
  }'

# Upload CV (PDF or DOCX, max 4MB)
curl -X POST /api/v1/applications/{id}/cv \
  -H "Authorization: Bearer $TOKEN" \
  -F "cv=@resume.pdf"

Authentication

POST /api/v1/auth/register

Register a new account (agency or job seeker). No auth required.

Rate limited: 5 per hour per IP, 2 per hour per email.

Request body:

FieldTypeRequiredDescription
legalNamestringYesBusiness or full name
contactNamestringYesContact person name
emailstringYesEmail address
phonestringNoPhone number
addressstringNoAddress
countrystringYesCountry
passwordstringYesPassword (min 8 chars)
agreedToTermsbooleanYesMust be true — confirms agreement to Terms of Service and Privacy Policy

Response (201):

{
  "data": {
    "message": "Registration successful. An access token has been sent to your email.",
    "email": "jane@acme.com",
    "tenantId": "...",
    "userId": "...",
    "tokenExpiresIn": "3 hours",
    "note": "Use the token from your email as: Authorization: Bearer <token>"
  }
}

POST /api/v1/auth/login

Request a new login token via email. No auth required.

Request body:

{ "email": "jane@acme.com" }

Response:

{ "data": { "message": "If this email is registered, an access token has been sent.", "tokenExpiresIn": "3 hours" } }

GET /api/v1/auth/me

Get current user and tenant info. Auth: Bearer


System Messages

All authenticated API responses may include a

messages
array with platform announcements:

{
  "data": { "..." : "..." },
  "meta": { "requestId": "..." },
  "messages": [
    {
      "id": "system-free-period",
      "title": "Free Job Posting Period",
      "body": "Payment is currently waived. Post jobs for free!",
      "audience": "all",
      "expiresAt": "2026-12-31T00:00:00.000Z"
    }
  ]
}

Messages are targeted by audience:

all
,
poster
, or
seeker
.


Jobs API

POST /api/v1/jobs

Create a new job (draft status). Auth: Bearer

Content moderation: Job title and description are automatically checked by AI. If flagged, the job is rejected and your account is blocked.

If

closingDate
is not provided, it is auto-calculated based on platform settings (default: 100 days).

FieldTypeRequiredDescription
titlestringYesJob title
jobReferencestringYesYour reference code
descriptionstringYesFull job description
employmentTypestringYesfull_time, part_time, contract, temporary, internship
workModestringYesremote, onsite, hybrid
countrystringYesCountry
locationstringNoCity/area
clientNamestringNoClient company name
salaryMinnumberNoMinimum salary
salaryMaxnumberNoMaximum salary
currencystringNo3-letter currency code
skillsstring[]NoRequired skills
experienceLevelstringNoentry, mid, senior, lead, executive
educationstringNoEducation requirements
closingDatestringNoISO date (auto-set if omitted)
applicationNotesstringNoApplication instructions

GET /api/v1/jobs

List your jobs. Auth: Bearer. Query:

page
,
pageSize
,
status
.

GET /api/v1/jobs/{jobId}

Get job details. Auth: Bearer

PATCH /api/v1/jobs/{jobId}

Update a job. Auth: Bearer. Content moderation applies to title/description changes.

GET /api/v1/jobs/search

Search published jobs. Auth: Bearer

ParamDescription
keywordSearch in title, description, client
locationFilter by location
workModeremote, onsite, hybrid
employmentTypefull_time, part_time, etc.
salaryMin / salaryMaxSalary range filter
experienceLevelentry, mid, senior, lead, executive
skillsComma-separated list
countryCountry filter
postedAfterISO date
page / pageSizePagination

POST /api/v1/jobs/{jobId}/pay

Initiate payment for a job. Auth: Bearer

Pricing: $30 per job post (admin configurable). During free posting periods, payment is waived.

Response (free period):

{
  "data": {
    "checkoutUrl": null,
    "amount": 0,
    "currency": "usd",
    "duration": "100 days",
    "freeJob": true,
    "message": "Free job period: 8 free posts remaining"
  }
}

Response (paid):

{
  "data": {
    "checkoutUrl": "https://checkout.stripe.com/...",
    "amount": 30,
    "currency": "usd",
    "duration": "100 days"
  }
}

POST /api/v1/jobs/{jobId}/publish

Publish a paid job. Auth: Bearer. Job must be in

paid_unpublished
status.

PATCH /api/v1/jobs/{jobId}/status

Change job status. Auth: Bearer

TransitionAllowed
published -> pausedYes
published -> closedYes
paused -> publishedYes
paused -> closedYes
closed -> archivedYes

GET /api/v1/jobs/{jobId}/applications

List applications for your job. Auth: Bearer (job owner)

Query:

page
,
pageSize
,
status

GET /api/v1/jobs/{jobId}/applications/{applicationId}

Get single application with full CV markdown. Auth: Bearer (job owner)

POST /api/v1/jobs/{jobId}/applications/shortlist

Batch shortlist applications for a job. Auth: Bearer (job owner)

Maximum 5 applications per request. All must be in

delivered
status.

Request body:

FieldTypeRequiredDescription
applicationIdsstring[]Yes1-5 application IDs to shortlist

Response (200):

{
  "data": {
    "shortlisted": 3,
    "applicationIds": ["app-1", "app-2", "app-3"]
  }
}

Applications API

POST /api/v1/applications

Submit application. Auth: Bearer. Job must be published.

FieldTypeRequired
jobIdstringYes
applicantNamestringYes
applicantEmailstringYes
applicantPhonestringNo
coverNotestringNo

POST /api/v1/applications/{id}/cv

Upload CV. Auth: Bearer. Multipart form data, field name:

cv
. Accepts PDF/DOCX, max 4MB.

The CV is automatically converted to Markdown and made available to the job poster.

GET /api/v1/applications/{id}

Get application status. Auth: Bearer

GET /api/v1/applications

List your applications. Auth: Bearer. Query:

page
,
pageSize
,
status
,
jobId
.


Payments API

GET /api/v1/payments

Payment history with Stripe receipt URLs. Auth: Bearer

Query:

page
,
pageSize

{
  "data": [{
    "id": "pay_abc",
    "jobId": "job_xyz",
    "jobTitle": "Senior Developer",
    "amount": "30",
    "currency": "usd",
    "status": "paid",
    "stripeReceiptUrl": "https://pay.stripe.com/receipts/...",
    "paidAt": "2026-04-23T10:30:00.000Z"
  }],
  "meta": { "page": 1, "pageSize": 20, "total": 5 }
}

Agent Integration

You don't need to write API glue code. The platform publishes ready-to-load skills and tool schemas at well-known URLs. Hermes, OpenClaw, Claude Skills, OpenAI tools, LangGraph, Smolagents — they all accept one of these shapes.

Three URLs, one bootstrap

URLReturnsWhen to fetch
{APP_URL}/.well-known/agent.json
Full API manifest (endpoints, workflows, errors)Once at startup, for documentation context
{APP_URL}/.well-known/tools
OpenAI/Hermes-compatible function-calling tool schemasOnce at startup, register with your model
{APP_URL}/.well-known/skills
Index of high-level skills, each with a markdown URLOnce at startup, optionally pre-load each skill
{APP_URL}/api/mcp
MCP server (Streamable HTTP, JSON-RPC)Configure as an MCP connector in Claude Desktop / Claude.ai

Hermes (Function Calling)

Fetch

{APP_URL}/.well-known/tools
once. The response is the modern OpenAI tools format that Nous Hermes accepts unchanged:

{
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "create_job",
        "description": "Create a draft job posting...",
        "parameters": { "type": "object", "properties": { "title": { "type": "string" } }, "required": ["title", "jobReference", ...] }
      },
      "metadata": { "method": "POST", "path": "/jobs", "requiresAuth": true }
    },
    ...
  ]
}

When Hermes emits a tool call, look up

metadata.method
and
metadata.path
, substitute path params, and POST/GET against
{APP_URL}/api/v1{path}
with your Bearer token. That's the entire integration.

MCP Server (Claude Desktop, Claude.ai, etc.)

For clients that speak the Model Context Protocol natively, point them at:

{APP_URL}/api/mcp

This is a streamable-HTTP MCP server: a single JSON-RPC endpoint, stateless, no SSE required. The same registry that powers

{APP_URL}/.well-known/tools
powers this — every tool, parameter constraint, and audience tag is identical.

Quick connect

ClientHow to add
Claude DesktopSettings → Developer → "Add MCP Server" → Streamable HTTP → paste the URL → save.
Claude.ai (web)Settings → Connectors → "Add custom connector" → paste the URL → save.
Programmatic clientsPOST JSON-RPC to the URL directly (see protocol section below).

Authentication

Bearer token, passed on the HTTP

Authorization
header of each MCP request. The token is the same one you get from
POST /api/v1/auth/register
or
POST /api/v1/auth/login
(3-hour TTL).

There are two convenient ways to obtain the token:

  1. Bootstrap via MCP itself
    register_account
    is the only tool that doesn't require auth. Call it once from your client (e.g. ask Claude: "register me on S5Jobs"), copy the returned token from the tool result, and paste it into the connector's auth setting.
  2. Out-of-band
    curl -X POST {APP_URL}/api/v1/auth/register …
    , then paste the token.

In TEST_MODE the token comes back in the response body. In production, it's emailed to the address you registered with.

What's exposed

All 16 tools from the registry. The server tags each one with

audience
(
poster
/
seeker
/
either
/
ops
) and
requiresAuth
so MCP clients with a typed UI can group them.

AudienceTools
Either
register_account
,
request_login_token
,
whoami
,
configure_handover_webhook
Job posters
create_job
,
update_job
,
get_job
,
list_my_jobs
,
pay_for_job
,
publish_job
,
list_applications_for_job
,
get_application
,
shortlist_applications
Job seekers
search_jobs
,
submit_application
,
list_my_applications

Tool input schemas are full JSON Schema with constraints (

minLength
,
maxLength
, enums, etc.) — Claude validates inputs against them before dispatch.

Protocol

Stateless JSON-RPC 2.0 over HTTPS. Methods supported:

MethodPurpose
initialize
Capability handshake. Returns
protocolVersion: "2025-06-18"
,
capabilities.tools
,
serverInfo
, and free-text
instructions
for the model.
notifications/initialized
No-op, ack only.
ping
No-op, ack only.
tools/list
Returns every tool with name, description, JSON-Schema
inputSchema
, and an
annotations
block (
title
,
readOnlyHint
,
audience
,
requiresAuth
).
tools/call
Dispatch. Body
{ name, arguments }
. Result is
{ content: [{ type: "text", text }], isError, structuredContent }
.
resources/list
/
prompts/list
Return empty arrays — we don't expose resources or prompts yet.

Anything else returns JSON-RPC error

-32601
(Method not found).

Discovery

A plain

GET
on the same URL returns a small human-readable JSON blob:

{
  "protocol": "Model Context Protocol (Streamable HTTP)",
  "protocolVersion": "2025-06-18",
  "serverInfo": { "name": "S5Jobs", "version": "1.0.0" },
  "endpoint": "{APP_URL}/api/mcp",
  "tools_count": 16,
  "auth": { "type": "bearer", "header": "Authorization: Bearer <token>", "obtain": "..." },
  "docs": "{APP_URL}/docs#mcp-server-claude-desktop-claudeai-etc"
}

Useful for health-checking the connector before hooking up a client.

End-to-end curl example

Register, then use the token to create a job — both via MCP:

# 1. tools/call register_account (no auth)
curl -s -X POST {APP_URL}/api/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0", "id": 1, "method": "tools/call",
    "params": {
      "name": "register_account",
      "arguments": {
        "legalName": "Acme Recruiting", "contactName": "Alex",
        "email": "alex@acme.example", "country": "United Kingdom",
        "password": "TempPassword123!", "agreedToTerms": true
      }
    }
  }'
# Response carries { "result": { "content": [{ "text": "..." }], "structuredContent": { "data": { "token": "..." } } } }

# 2. tools/call create_job — pass the token on the HTTP Authorization header
TOKEN=...
curl -s -X POST {APP_URL}/api/mcp \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "jsonrpc": "2.0", "id": 2, "method": "tools/call",
    "params": {
      "name": "create_job",
      "arguments": {
        "title": "Senior Backend Engineer", "jobReference": "JOB-2026-100",
        "description": "Build reliable services...", "employmentType": "full_time",
        "workMode": "remote", "country": "United Kingdom"
      }
    }
  }'

Dispatch semantics

When the MCP server receives

tools/call
:

  1. Looks up the tool by name in the registry.
  2. Resolves path params from the arguments (
    {jobId}
    → encoded value, removed from the body).
  3. For
    GET
    endpoints, remaining arguments become query parameters; for
    POST
    /
    PATCH
    , they become the JSON body.
  4. Forwards the request to
    {APP_URL}/api/v1/<path>
    , passing the Authorization header through unchanged.
  5. Returns the response as MCP
    content
    (a JSON-stringified text block) and
    structuredContent
    (the parsed object), with
    isError
    reflecting the underlying HTTP status.

So an MCP client never bypasses our normal validation, content-moderation, or rate-limiting — it just reframes the same REST call.

Errors and the AWAITING_HUMAN flow

If a tool returns 4xx/5xx, the MCP result has

isError: true
and the JSON error body is in
content[0].text
.
AWAITING_HUMAN
errors keep their full shape:

{
  "error": {
    "code": "AWAITING_HUMAN",
    "message": "An open issue (dispute) requires resolution before this job can be published.",
    "fields": {
      "issueId": "...",
      "issueType": "dispute",
      "humanResumeUrl": "{APP_URL}/h/abc123"
    }
  }
}

The client model should pause its automation, show the operator the

humanResumeUrl
, and retry the call after they've resolved (or after the
human_resolved
tenant webhook fires).

OpenClaw / Claude Skills / generic tool-using agents

Fetch

{APP_URL}/.well-known/skills
. You'll get an index like:

{
  "skills": [
    { "name": "post-a-job",       "url": "{APP_URL}/.well-known/skills/post-a-job" },
    { "name": "apply-for-a-job",  "url": "{APP_URL}/.well-known/skills/apply-for-a-job" },
    { "name": "handle-handover",  "url": "{APP_URL}/.well-known/skills/handle-handover" }
  ],
  "tools_url": "{APP_URL}/.well-known/tools"
}

Each skill URL returns a self-contained markdown bundle with frontmatter (Anthropic Skills convention). Drop the markdown into your agent's skill folder; the frontmatter lists the tools the skill needs by name. Combine with

/.well-known/tools
and the agent has everything: prompt instructions + tool schemas + failure-mode handling, including the
AWAITING_HUMAN
branch.

Operator handoff UX

When your agent gets a

409 AWAITING_HUMAN
error or a
human_required
webhook, surface this to your operator (this exact message is the tested pattern):

A payment needs you to step in: {summary}. Please open this link to approve or reject — I'll continue once you do.

{APP_URL}/h/abc123

Don't loop the agent — wait for the

human_resolved
webhook callback (or have the operator ping you when done).


Content Moderation

This platform uses AI-powered content moderation to protect users and maintain safety.

All job postings (title + description) are automatically screened by an AI safety model. This applies on both job creation and job updates.

How It Works

  1. You submit a job posting via the API
  2. The content is checked by the AI safety model
  3. If safe -> job is created normally
  4. If unsafe -> job is rejected, your account is immediately blocked, a warning email is sent, and an admin issue is created

What Happens When Blocked

  • All your Bearer tokens are immediately invalidated
  • You cannot log in or access any API endpoints
  • An admin must manually review and unblock your account
  • For dangerous content (violence, exploitation, weapons), law enforcement authorities will be notified

Categories Checked

Violence, hate speech, discrimination, illegal activities, sexual exploitation, child safety, weapons, self-harm, fraud, privacy violations, and other harmful content.

For AI Agents

If you are an AI agent: You MUST implement your own content safety checks before submitting job postings. Do not attempt to test the moderation system. Violations are treated the same regardless of intent. Your account will be blocked on the first violation.


Error Handling

All errors follow this format:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid request body",
    "fields": { "email": ["Email is required"] }
  },
  "meta": { "requestId": "..." }
}

Error Codes

CodeStatusDescription
VALIDATION_ERROR400Invalid request data
UNAUTHORIZED401Missing or invalid token
FORBIDDEN403Insufficient permissions
NOT_FOUND404Resource not found
DUPLICATE409Duplicate resource
RATE_LIMITED429Too many requests
TENANT_SUSPENDED403Tenant account suspended
PAYMENT_REQUIRED402Payment needed
LIMIT_EXCEEDED400Max limit reached (e.g., 4 agents)
CONTENT_REJECTED403Content policy violation -- account blocked
INTERNAL_ERROR500Server error

Additional API endpoints for advanced integrations (webhooks, receiving agents) are available. Contact support for details.

Request & Response Shapes (live)

Pulled from /.well-known/agent.json, which is generated from the same Zod schemas the runtime validates against. If a schema changes in the codebase, this list updates on the next request — no documentation drift.

Loading auto-generated schemas…