Skip to main content

Submit Job Application

NL EN

Updated June 15, 2026

POST /api/v1/applications
curl -X POST \
  "https://app.recruitsome.com/api/v1/applications" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Accept: application/json"
const response = await fetch('https://app.recruitsome.com/api/v1/applications', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Accept': 'application/json',
  },
});

const data = await response.json();
console.log(data);
use Illuminate\Support\Facades\Http;

$response = Http::withToken('YOUR_API_KEY')
    ->acceptJson()
    ->post('https://app.recruitsome.com/api/v1/applications');

$data = $response->json();

Submit a candidate's application to a published vacancy — typically from the application form on your career website. One call delivers the candidate's details, their resume and other documents, and tracking information; Recruitsome creates the application, attaches the documents, and starts resume parsing automatically.

Authentication

Requires an API key with the applications:write scope (included in the Career Website preset). Calls with a key that lacks the scope return a 403 with error code INSUFFICIENT_SCOPE.

Tip

An application a candidate typed in is irreplaceable — don't let a network hiccup or a rate limit lose it. Store the submission locally (queue, database, or browser storage) before calling the API, and retry with backoff on any failure. Only discard your local copy after you receive the 201 response.

Body

FieldTypeRequiredDescription
vacancystring or integeryesVacancy identifier: a publication slug, a vacancy slug, or a numeric vacancy ID (see below)
candidateobjectyesThe applicant's details
candidate.given_namestringyesFirst/given name (max 255 chars)
candidate.family_namestringyesLast/family name (max 255 chars)
candidate.emailstringyesValid email address (max 255 chars)
candidate.mobile_phonestringnoMobile number in international format (see phone notes below)
candidate.fixed_phonestringnoLandline number in international format
candidate.linkedin_urlstringnoLinkedIn profile URL — must be a valid URL (max 255 chars)
documentsobjectnoDocuments to attach (see below)
privacy_policy_acceptedbooleannoWhether the candidate accepted your privacy policy; recorded as consent with a timestamp
trackingobjectnoSubmission context
tracking.ip_addressstringnoThe candidate's IP address — must be a valid IP
tracking.user_agentstringnoBrowser user agent (max 1000 chars)
tracking.referrerstringnoReferral URL — must be a valid URL (max 1000 chars)

Vacancy identification

The vacancy field accepts three identifiers, resolved in this order:

  1. Numeric vacancy ID: 123
  2. Publication slug: "maintenance-engineer-amsterdam" — as returned by GET /vacancies
  3. Vacancy slug: "maintenance-engineer"

Tip

Use the publication slug from the vacancy list API. It's the only identifier that lets Recruitsome attribute the application to the specific publication channel it came from.

Documents

Each document is an object with filename (max 255 chars), content (base64 encoded file data), and mime_type:

FieldAllowed MIME typesMax size (decoded)
documents.resumeapplication/pdf, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/zip, application/x-ole-storage, application/octet-stream5MB
documents.cover_letterSame as resume5MB
documents.additional (array, max 5)Any MIME type string20MB each

Each entry in documents.additional may also carry a description (max 500 chars).

The application/zip, application/x-ole-storage, and application/octet-stream types are accepted because browsers and older Word files often misreport .doc/.docx MIME types — pass the type your file API gives you. Base64 encoding inflates file size by ~33%, and the total request body is capped at 30MB.

Phone numbers

mobile_phone and fixed_phone are validated as international phone numbers with lenient parsing: spaces and dashes are fine (+31 6 1234 5678 and +31-6-12345678 both pass), but the number must carry its country code — the leading + is what matters. A national-format number like 0612345678 fails with:

TEXT
The mobile phone number must be in valid international format (e.g., +1234567890).

Example

curl -X POST https://app.recruitsome.com/api/v1/applications \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "vacancy": "maintenance-engineer-amsterdam",
    "candidate": {
      "given_name": "Emma",
      "family_name": "de Vries",
      "email": "[email protected]",
      "mobile_phone": "+31612345678"
    },
    "documents": {
      "resume": {
        "filename": "emma_de_vries_cv.pdf",
        "content": "JVBERi0xLjQKJeLj...",
        "mime_type": "application/pdf"
      }
    },
    "privacy_policy_accepted": true,
    "tracking": {
      "ip_address": "84.241.196.34",
      "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
      "referrer": "https://www.yourcompany.com/vacatures/maintenance-engineer"
    }
  }'
const resumeFile = document.getElementById('resume-input').files[0];

const toBase64 = (file) =>
  new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = () => resolve(reader.result.split(',')[1]);
    reader.onerror = reject;
  });

const response = await fetch('https://app.recruitsome.com/api/v1/applications', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    vacancy: 'maintenance-engineer-amsterdam',
    candidate: {
      given_name: 'Emma',
      family_name: 'de Vries',
      email: '[email protected]',
      mobile_phone: '+31612345678',
    },
    documents: {
      resume: {
        filename: resumeFile.name,
        content: await toBase64(resumeFile),
        mime_type: resumeFile.type,
      },
    },
    privacy_policy_accepted: true,
  }),
});

if (response.status === 201) {
  const result = await response.json();
  console.log('Application submitted:', result.data.id);
}

Response 201

JSON
{
  "message": "Application submitted successfully",
  "data": {
    "id": 456,
    "vacancy": {
      "id": 123,
      "slug": "maintenance-engineer",
      "title": "Maintenance Engineer"
    },
    "applicant": {
      "id": null,
      "given_name": "Emma",
      "family_name": "de Vries",
      "email": "[email protected]"
    },
    "status": "screening",
    "source": "api",
    "documents": [
      {
        "id": 789,
        "category": "resume",
        "filename": "emma_de_vries_cv.pdf",
        "size": 245120,
        "mime_type": "application/pdf"
      }
    ],
    "submitted_at": "2026-06-11T14:30:00+00:00",
    "created_at": "2026-06-11T14:30:00+00:00"
  }
}

The applicant.id is the linked candidate's ID when the applicant is already known as a candidate in Recruitsome, and null for a first-time applicant. Each entry in documents describes a stored document: id, category (resume, cover_letter, or other for additional documents), filename, size in bytes, and mime_type.

Errors

Validation errors 422

JSON
{
  "message": "The given data was invalid.",
  "errors": {
    "candidate.email": [
      "The candidate's email address is required."
    ],
    "candidate.mobile_phone": [
      "The mobile phone number must be in valid international format (e.g., +1234567890)."
    ],
    "documents.resume": [
      "The file size exceeds the maximum allowed limit."
    ]
  }
}

Vacancy not found 404

JSON
{
  "message": "Vacancy not found",
  "error": "VACANCY_NOT_FOUND"
}

Vacancy closed 400

The vacancy exists but is no longer accepting applications:

JSON
{
  "message": "This vacancy is no longer accepting applications",
  "error": "VACANCY_CLOSED"
}

Other business-rule rejections return a 400 with "error": "VALIDATION_ERROR" and the reason in message.

Authentication and authorization

StatusBodyWhen
401{"message": "Unauthenticated."}Missing or invalid API key
403{"message": "This API key does not have the required permission: applications:write", "error": "INSUFFICIENT_SCOPE", "required_scope": "applications:write"}API key lacks the applications:write scope

Rate limiting 429

The endpoint accepts 10 requests per minute per API key. Exceeding it returns the standard Laravel throttle response with a Retry-After header (seconds until the window resets):

JSON
{
  "message": "Too Many Attempts."
}

Server error 500

JSON
{
  "message": "An unexpected error occurred while processing your application",
  "error": "SUBMISSION_FAILED"
}

A 500 can occur before or after the application was stored — the API does not deduplicate submissions, so don't retry blindly. Retry once, and if your retry returns a duplicate-looking success, reach out to [email protected] to deduplicate.

You're handling personal data: only collect what the vacancy genuinely needs, and pass privacy_policy_accepted so the candidate's consent is recorded with a timestamp in Recruitsome.