Public API

Contractor job submission and localized option lists use /api/contractor/* (legacy /api/employer/* uses the same handlers). Mediators authenticate with POST /api/mediator/login (Laravel Sanctum bearer token). Example base URL: https://api.digitemp.ch/api or your app host. This page is served at the app root: /.

Reference

Route index

Replace {prefix} with contractor or employer. Verification is available on both contractor and employer paths.

  • GET /api/{prefix}/industries
  • GET /api/{prefix}/professions?industry=<industryKey>
  • GET /api/{prefix}/locations
  • GET /api/{prefix}/job-types
  • GET /api/{prefix}/durations
  • GET /api/{prefix}/qualifications
  • GET /api/{prefix}/mobility-options
  • GET /api/{prefix}/experience-options
  • POST /api/{prefix}/jobs
  • GET /api/contractor/verify/{token} (also /api/employer/verify/{token})
  • POST /api/mediator/login — mediator token (public)
  • GET /api/mediator/cantons — cantons with ≥1 open job (auth)
  • GET /api/mediator/jobs?canton=ZH — open jobs in canton (auth)
  • GET|POST|PATCH|DELETE /api/mediator/candidates … (auth)
  • GET|POST /api/mediator/suggestions (auth)

Mediator

POST /api/mediator/login

Issues a personal access token for an active user whose role is mediator. This is login only; there is no public signup endpoint on this API.

  • Send Content-Type: application/json and (for browser clients) Accept: application/json.
  • Rate limit: 5 requests per minute per IP and email address.
  • Requires the personal_access_tokens table (run php artisan migrate after deploying Sanctum).

Request body

{
  "email": "mediator@example.com",
  "password": "your-password"
}

200 — success

{
  "token": "1|…plain-text-token…",
  "token_type": "Bearer",
  "user": {
    "id": 1,
    "name": "Example Mediator",
    "email": "mediator@example.com",
    "preferred_lang": "de"
  }
}

Use later API calls as: Authorization: Bearer <token> (middleware auth:sanctum on protected routes).

401 — invalid credentials (wrong password, unknown email, user not a mediator, or is_active is false; same message in all cases):

{
  "message": "Invalid credentials."
}

422 — validation (e.g. invalid email format, missing password).

Mediator — authenticated routes

All paths below require Authorization: Bearer <token> from login. User must have role: mediator and be active; otherwise 403.

  • GET /api/mediator/cantons{ "cantons": [ { "code", "name", "open_job_count" } ] }. Names use the user’s preferred_lang (de/fr/en).
  • GET /api/mediator/jobs?canton=ZH — same filter as canton list: status = published, non-empty canton_code, not past expires_at. Query canton or canton_code (2 letters, trimmed, uppercased). Optional page, per_page (max 50). Response: data (job cards) and meta including canton (resolved code), current_page, last_page, per_page, total. Each job includes mobility (localized label from the stored value) and licence_categories (array of licence codes, may be empty).
  • GET /api/mediator/candidates — list candidates where managed_by is the current user.
  • POST /api/mediator/candidates — create; optional skill_ids array of skill IDs to attach. Use driving_licence + licence_categories for mobility. Response includes read-only mobility (localized summary from the licence; do not send mobility or mobility_id on write).
  • GET|PATCH|DELETE /api/mediator/candidates/{id} — detail, update, delete (404 if not owned).
  • POST /api/mediator/suggestions — body { "job_id", "candidate_id" }. Creates a pending suggestion; issues candidate_view_token on the job when missing (employer picker link). 422 with code: DUPLICATE_SUGGESTION if an active suggestion already exists for that job and candidate.
  • GET /api/mediator/suggestions — recent suggestions by this mediator (up to 200).

Candidate create request example (POST /api/mediator/candidates):

{
  "first_name": "Anna",
  "last_name": "Muster",
  "birthdate": "1994-03-21",
  "address": "Musterstrasse 10, 4051 Basel",
  "mobile_number": "+41 79 123 45 67",
  "email": "anna@example.com",
  "languages": ["de", "en", "fr"],
  "driving_licence": true,
  "licence_categories": ["B", "BE"],
  "hourly_rate": 48.5,
  "experiences": [
    { "from_month": "01/2020", "to_month": "12/2023", "description": "4 years in interior masonry" },
    { "from_month": "01/2018", "to_month": "12/2019", "description": "2 years in facade restoration" }
  ],
  "notes": "Available on short notice.",
  "is_active": true,
  "skill_ids": [1, 3, 7]
}

201 — success:

{
  "data": {
    "id": 42,
    "first_name": "Anna",
    "last_name": "Muster",
    "full_name": "Anna Muster",
    "birthdate": "1994-03-21",
    "address": "Musterstrasse 10, 4051 Basel",
    "mobile_number": "+41 79 123 45 67",
    "email": "anna@example.com",
    "languages": ["de", "en", "fr"],
    "driving_licence": true,
    "licence_categories": ["B", "BE"],
    "mobility": "B, BE",
    "hourly_rate": 48.5,
    "experiences": [
      { "from_month": "01/2020", "to_month": "12/2023", "description": "4 years in interior masonry" },
      { "from_month": "01/2018", "to_month": "12/2019", "description": "2 years in facade restoration" }
    ],
    "skills": ["Maurer", "Sicherheitskenntnisse"],
    "notes": "Available on short notice.",
    "is_active": true
  }
}

422 — validation shape:

{
  "message": "The name field is required. (and 1 more error)",
  "errors": {
    "name": ["The name field is required."],
    "canton_code": ["The selected canton code is invalid."]
  }
}

Contractor

GET /api/contractor/industries

Returns available industries with stable keys and multilingual labels.

[
  { "value": "technologie", "label": { "de": "IT & Tech", "en": "IT & Tech", "fr": "IT & Tech" } },
  { "value": "finanzen", "label": { "de": "Finanzen", "en": "Finance", "fr": "Finance" } }
]
GET /api/contractor/professions?industry=…

Example: /api/contractor/professions?industry=technologie — returns professions allowed for the selected industry key.

[
  { "value": "software-entwickler", "label": { "de": "Software-Entwickler", "en": "Software Developer", "fr": "Developpeur logiciel" } },
  { "value": "projektmanager", "label": { "de": "Projektmanager", "en": "Project Manager", "fr": "Chef de projet" } }
]
GET /api/contractor/locations

Work location list by canton code.

[
  { "value": "ZH", "label": { "de": "Zurich", "en": "Zurich", "fr": "Zurich" } }
]
GET /api/contractor/job-types

Job type options (localized labels).

GET /api/contractor/durations

Expected duration options (localized labels).

GET /api/contractor/qualifications

Fixed list of four options. Submit value in POST /jobs as qualification. Legacy German words may still be accepted server-side; clients should use these keys.

[
  { "value": "skilled", "label": { "de": "Gelernt", "en": "Skilled worker", "fr": "Ouvrier qualifié" } },
  { "value": "semi-skilled", "label": { "de": "Angelernt", "en": "Semi-skilled worker", "fr": "Ouvrier semi-qualifié" } },
  { "value": "professional-worker", "label": { "de": "Berufsarbeiter", "en": "Professional worker", "fr": "Ouvrier de métier" } },
  { "value": "general-laborer", "label": { "de": "Hilfsarbeiter", "en": "General laborer", "fr": "Manœuvre" } }
]
GET /api/contractor/mobility-options

Mobility options (localized labels).

GET /api/contractor/experience-options

Years of experience options (localized labels).

POST /api/contractor/jobs

Creates a pending job, validates industry–profession dependency, and sends the verification email.

  • Required: canonical contractor payload fields (see example)
  • Uses stable keys for industry and profession
  • Optional: licence_categories — array of EU/CH driving-licence codes (A, A1, A2, B, BE, C, C1, CE, C1E, D, D1, DE, G). Multiple selections allowed. Also accepted as a comma-separated string (e.g. "B, BE").
  • Returns 201 on success
{
  "employer_company": "Digitemp AG",
  "employer_contact_person": "Max Muster",
  "employer_email": "hr@digitemp.ch",
  "canton_code": "ZH",
  "location": "zurich",
  "job_type_id": "1",
  "start_date": "2026-04-01",
  "duration_id": "1",
  "industry": "technologie",
  "profession": "software-entwickler",
  "qualification": "skilled",
  "mobility": "flexibel",
  "licence_categories": ["B", "BE"],
  "experience_years": "3+",
  "remarks": "Optional note",
  "employer_lang": "de"
}
{
  "message": "Job created successfully"
}
422 returns Validation failed and may return INVALID_PROFESSION_FOR_INDUSTRY for mapping errors.
GET /api/contractor/verify/{token}

Verifies a job posting token and publishes the job. Same behavior at /api/employer/verify/{token}.

  • Browser (default): localized HTML result page.
  • API clients: send Accept: application/json for JSON.
  • Invalid token: INVALID_VERIFICATION_TOKEN
  • Expired job: JOB_EXPIRED
  • On success: published, expires_at = now + 7 days
Validation error shape (422)
{
  "message": "Validation failed",
  "errors": {
    "employer_email": ["The employer email field must be a valid email address."],
    "industry": ["The industry field is required."]
  }
}
Invalid profession for industry (422)
{
  "code": "INVALID_PROFESSION_FOR_INDUSTRY",
  "message": "Profession does not belong to selected industry",
  "field": "profession",
  "allowedProfessions": [
    { "value": "projektmanager", "label": { "de": "Projektmanager", "en": "Project Manager", "fr": "Chef de projet" } }
  ]
}
Example curl commands

Replace host if needed (examples use https://api.digitemp.ch).

# Option lists (swap /contractor/ for /employer/)
curl -i "https://api.digitemp.ch/api/contractor/industries"
curl -i "https://api.digitemp.ch/api/contractor/professions?industry=technologie"
curl -i "https://api.digitemp.ch/api/contractor/locations"
curl -i "https://api.digitemp.ch/api/contractor/job-types"
curl -i "https://api.digitemp.ch/api/contractor/durations"
curl -i "https://api.digitemp.ch/api/contractor/qualifications"
curl -i "https://api.digitemp.ch/api/contractor/mobility-options"
curl -i "https://api.digitemp.ch/api/contractor/experience-options"

# Create job
curl -i -X POST "https://api.digitemp.ch/api/contractor/jobs" \
  -H "Content-Type: application/json" \
  -d '{"employer_company":"Digitemp AG","employer_contact_person":"Max Muster","employer_email":"hr@digitemp.ch","canton_code":"ZH","location":"zurich","job_type_id":"1","start_date":"2026-04-01","duration_id":"1","industry":"technologie","profession":"software-entwickler","qualification":"skilled","mobility":"flexibel","licence_categories":["B","BE"],"experience_years":"3+","remarks":"Optional note","employer_lang":"de"}'

# Verify (HTML in browser)
curl -i "https://api.digitemp.ch/api/contractor/verify/YOUR_TOKEN_HERE"

# Verify as JSON
curl -i -H "Accept: application/json" "https://api.digitemp.ch/api/contractor/verify/YOUR_TOKEN_HERE"

# Mediator login (JSON)
curl -i -X POST "https://api.digitemp.ch/api/mediator/login" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"email":"mediator@example.com","password":"your-password"}'
Backward compatibility

GET and POST routes above exist for both /api/contractor/… and /api/employer/…. Verification: GET /api/contractor/verify/{token} (used in emails) or GET /api/employer/verify/{token}.