REST API · DDD + CQRS · Async

Post jobs.
Ship in 2 hours.

A production-ready job posting API with async queues, Bearer token auth and rate limiting. Zero friction, full control.

Read the Docs Get API Key
POST /api/v1/jobs
$ curl -X POST https://api.2hr.pl/api/v1/jobs \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"title":"Senior PHP Dev","location":"Remote","snippet":"We are hiring!","link":"https://example.com/apply","salary":{"type":"monthly","min":15000,"max":20000,"currency":"PLN"}}'
# Response 202 Accepted
{ "message": "Job creation queued", "status": "pending" }
$ Worker picks it up → status=published
10/min
Rate Limit
<50ms
Avg Response
202
Async Create
Hashed
Token Storage

API Reference

All Endpoints

Every route requires a Bearer token unless marked Public.

GET
/api/v1/jobs
List your published job postings, max 20, sorted by updated date.
sync auth
POST
/api/v1/jobs
Create a job posting. Queued via Redis → returns 202 immediately.
async auth
PUT
/api/v1/jobs/{uuid}
Update title, location, salary, link or snippet. Owner only.
sync auth
PUT
/api/v1/jobs/{uuid}/status
Change job status to ended or published.
sync auth
GET
/api/v1/companies
List your company profiles.
sync auth
POST
/api/v1/companies
Create a company profile linked to your account.
sync auth
PUT
/api/v1/companies/{uuid}
Update company information. Owner only.
sync auth
POST
/api/register
Register a new account (individual or company). IP-rate limited.
public

Built-in Features

Everything you need

Production-grade features out of the box. No configuration hell.

🔑
Bearer Token Auth
Tokens stored as secure hashes. Zero plaintext secrets in the database.
Async Job Queue
POST /jobs dispatches to an async queue. Worker processes in background - your client gets 202 immediately.
🛡️
Rate Limiting
10 requests per minute per token, stored in Redis. Returns 429 with retry_after header.
Moderation Flow
Unverified users get requires_review status. Admin approves before job goes live.
🏗️
DDD + CQRS
Clean domain model, command/query separation, repository pattern. Doctrine ORM for persistence only.
🚫
Spam Protection
Unverified accounts capped at 3 jobs/day. Blocked TLD list (.ru, .cn, .tk) on link field.

How job creation works

Non-blocking by design. The HTTP response is instant while processing happens in the background.

1
POST /api/v1/jobs
Controller validates the request and saves a Job record with status=pending.
2
Dispatch to Redis
CreateJobCommand is dispatched via Symfony Messenger to the async transport.
3
HTTP 202 Accepted
Client immediately gets a response. No waiting for heavy processing.
4
Worker processes
messenger:consume async picks up the message and runs CreateJobHandler.
5
Status updated
Verified user → published. Unverified → requires_review.
Quick Start Example PHP
// 1. Create a job posting $response = Http::withToken($token) ->post('https://api.2hr.pl/api/v1/jobs', [ 'title' => 'Senior PHP Developer', 'location' => 'Remote', 'snippet' => 'We are looking for...', 'link' => 'https://example.com/apply', 'type' => 'full-time', 'salary' => [ 'type' => 'monthly', 'min' => 15000, 'max' => 20000, 'currency' => 'PLN', ], ]); // 2. Returns 202 immediately // $response->status() === 202   // 3. Fetch published jobs $jobs = Http::withToken($token) ->get('https://api.2hr.pl/api/v1/jobs') ->json('data');   // 4. End a job posting Http::withToken($token) ->put("https://api.2hr.pl/api/v1/jobs/{$id}/status", [ 'status' => 'ended', ]);