Logo StartupKit
EN

API Reference

Authenticate and query the Hiring API for job postings, candidates, and applications.

Why It Matters

The Hiring API gives external systems read access to your hiring data — job boards pull published postings, HRIS systems sync candidate records, dashboards track pipeline metrics. Token scopes ensure each integration only sees what it needs.

Base URL

All API endpoints are accessed via:

https://startupkit.app/api/v1

Authentication

Every request requires an API token in the Authorization header:

Authorization: Bearer YOUR_TOKEN

Alternative format (both work):

Authorization: token YOUR_TOKEN

Create tokens at Settings > API. Each token must have explicit scopes to access hiring endpoints.

Scope Access
job_postings:read Job postings and stages
candidates:read Candidate PII (name, email, phone), application metafields, resume downloads
applications:read Applications, stage history, resume downloads

Tokens without any scopes cannot access hiring endpoints.

Token Format and Lifecycle

  • Format: 24-character base58 string (e.g., k3Jf9pQ2mZ7xR4nL8vT6bWyH)
  • Shown once: The raw token is displayed exactly once, on the confirmation screen right after you create it. Copy it then — it cannot be retrieved again.
  • Digest-only storage: Kit stores only a SHA-256 digest of the token, never the plaintext. If you lose the token, revoke it and create a new one; there is no way to recover the original value.
  • Last used tracking: Every successful request updates last_used_at
  • No expiry date: Tokens don’t expire on a timer. There is no expiration option on the creation form, and Settings > API shows no expiry — a token stays valid until it is revoked.
  • Inactivity alerts: Tokens unused for 30+ days trigger admin notifications
  • Auto-revocation: Tokens are automatically revoked when a user is removed from the account

Best practice: Create separate tokens per integration with minimal scopes needed.

Account Scoping

Each API token is bound to a specific account. The account is derived automatically from the token — no account_id parameter is needed.

When a team member is removed from an account, their tokens for that account are revoked automatically.

Error Responses

Status Meaning Common Causes
400 Bad request Account context missing (edge case in session-based auth)
401 Unauthorized Missing, invalid, or revoked token
403 Forbidden Token lacks required scope for this endpoint
404 Not found Resource doesn’t exist or doesn’t belong to your account
422 Unprocessable entity Validation errors (primarily for non-hiring endpoints)

Pagination

List endpoints return paginated results:

{
  "data": [...],
  "pagination": {
    "current_page": 1,
    "total_pages": 3,
    "total_count": 42,
    "per_page": 20
  }
}

Pass ?page=2 to fetch subsequent pages.


Job Postings

Scope required: job_postings:read

List Job Postings

GET /api/v1/job_postings

Example:

curl -H "Authorization: Bearer YOUR_TOKEN" \
  https://startupkit.app/api/v1/job_postings?status=published

Parameters:

Param Type Description
status string Filter by draft, published, paused, or closed
page integer Page number

Response:

{
  "data": [
    {
      "id": "job_abc123",
      "title": "Senior Rails Developer",
      "status": "published",
      "department": "Engineering",
      "location": "New York, NY",
      "employment_type": "full_time",
      "remote": true,
      "salary_min": "120000.0",
      "salary_max": "180000.0",
      "salary_currency": "USD",
      "salary_period": "year",
      "published_at": "2026-01-15T10:00:00Z",
      "closed_at": null,
      "created_at": "2026-01-10T08:30:00Z",
      "updated_at": "2026-02-01T14:20:00Z",
      "stages": [
        {
          "id": "stg_def456",
          "name": "Screening",
          "stage_type": "default",
          "position": 0
        }
      ],
      "counts": {
        "total_applications": 24,
        "active_applications": 18,
        "rejected": 6
      }
    }
  ],
  "pagination": { ... }
}

Get Job Posting

GET /api/v1/job_postings/:id

Returns the same shape as an item in the list response.

List Stages

GET /api/v1/job_postings/:job_posting_id/stages

Response:

{
  "data": [
    {
      "id": "stg_def456",
      "name": "Screening",
      "stage_type": "default",
      "position": 0
    }
  ]
}

Stages are ordered by position. No pagination.


Candidates

Scope required: candidates:read

List Candidates

GET /api/v1/candidates

Example:

curl -H "Authorization: Bearer YOUR_TOKEN" \
  https://startupkit.app/api/v1/[email protected]

Parameters:

Param Type Description
search string Search by name or email
page integer Page number

Response:

{
  "data": [
    {
      "id": "cand_ghi789",
      "first_name": "John",
      "last_name": "Doe",
      "email": "[email protected]",
      "phone": "+1-555-0100",
      "status": "active",
      "github_username": "johndoe",
      "created_at": "2026-01-20T09:15:00Z",
      "applications": [
        {
          "id": "app_jkl012",
          "job_posting_id": "job_abc123",
          "job_posting_title": "Senior Rails Developer",
          "current_stage_name": "Interview",
          "status": "active",
          "submitted_at": "2026-01-20T09:15:00Z"
        }
      ]
    }
  ],
  "pagination": { ... }
}

Get Candidate

GET /api/v1/candidates/:id

Returns the same shape as an item in the list response.


Applications

Scope required: applications:read

List Applications

GET /api/v1/applications

Example:

curl -H "Authorization: Bearer YOUR_TOKEN" \
  https://startupkit.app/api/v1/applications?job_posting_id=job_abc123&status=active

Parameters:

Param Type Description
job_posting_id string Filter by job posting prefix ID
status string Filter by active or rejected
page integer Page number

Response:

{
  "data": [
    {
      "id": "app_jkl012",
      "job_posting_id": "job_abc123",
      "candidate_id": "cand_ghi789",
      "current_stage_name": "Interview",
      "status": "active",
      "submitted_at": "2026-01-20T09:15:00Z",
      "created_at": "2026-01-20T09:15:00Z",
      "updated_at": "2026-02-10T11:30:00Z",
      "candidate": {
        "id": "cand_ghi789",
        "first_name": "John",
        "last_name": "Doe",
        "email": "[email protected]"
      },
      "stage_history": [
        {
          "id": "sp_mno345",
          "stage_name": "Screening",
          "stage_type": "default",
          "status": "completed",
          "started_at": "2026-01-20T09:15:00Z",
          "completed_at": "2026-01-25T16:00:00Z"
        },
        {
          "id": "sp_pqr678",
          "stage_name": "Interview",
          "stage_type": "interview",
          "status": "in_progress",
          "started_at": "2026-01-25T16:00:00Z",
          "completed_at": null
        }
      ],
      "metafields": [
        {
          "key": "years_of_experience",
          "label": "Years of Experience",
          "field_type": "number",
          "value": "8",
          "source": "ai",
          "confidence": 0.92,
          "extracted_at": "2026-01-20T09:18:00Z",
          "edited": false,
          "original_value": null
        },
        {
          "key": "salary_expectation",
          "label": "Salary Expectation",
          "field_type": "number",
          "value": "150000",
          "source": "ai",
          "confidence": 0.75,
          "extracted_at": "2026-01-20T09:18:00Z",
          "edited": true,
          "original_value": "140000"
        }
      ]
    }
  ],
  "pagination": { ... }
}

Metafield Data

Scope required: candidates:read

Metafield values carry AI-extracted candidate PII, so the metafields array is gated on candidates:read — the same scope that gates the candidate block — and not on applications:read. A token with applications:read but without candidates:read receives application responses with no metafields key at all. The example response above is what a token holding both scopes sees.

When the token does hold candidates:read, each application includes a metafields array containing structured data fields extracted or entered for that application. Every entry has:

Field Type Description
key string Machine-readable identifier for the field
label string Human-readable display name
field_type string Data type — one of exactly these nine: text, textarea, number, date, select, boolean, url, rating, tags
value string | null Current value (always serialized as a string)
source string How the value was set — one of exactly two values: manual (entered or edited by a person) or ai (extracted or set by AI)
confidence float | null AI confidence score (0.0–1.0) when source is ai; null otherwise
extracted_at timestamp | null When the value was set or extracted
edited boolean true only when an AI-sourced value was later overwritten by something other than the raw AI extraction (in practice, an MCP hiring_update_metafield_value call). Edits made by a person in Kit’s own UI save source: manual and always report edited: false.
original_value string | null The original AI-extracted value before human edits; null if never edited

Within a token that has candidates:read, the metafields array is not filtered further by the managers_only visibility flag. Every metafield defined on the job posting — including fields marked managers_only in the job posting configuration — is returned, regardless of the token owner’s role. candidates:read is therefore the only control over metafield exposure: if a field holds sensitive information you don’t want exposed to an integration, don’t grant that integration candidates:read, since the API does not apply the manager-only visibility filtering used elsewhere in the app. Granting applications:read alone is safe in this respect — it returns stage history and application status without metafields.

Get Application

GET /api/v1/applications/:id

Returns the same shape as an item in the list response. When the token has candidates:read scope and the application has a resume attached, the response includes resume metadata:

{
  "id": "app_jkl012",
  "resume_filename": "john_doe_resume.pdf",
  "resume_content_type": "application/pdf",
  "resume_byte_size": 245760,
  ...
}

These fields are omitted when no resume is attached or when candidates:read scope is missing.

Download Resume

Scopes required: applications:read + candidates:read

GET /api/v1/applications/:application_id/resume

Downloads the candidate’s resume for the given application. Returns a 302 redirect to a time-limited signed URL (valid for 30 minutes).

Example:

curl -L -H "Authorization: Bearer YOUR_TOKEN" \
  -o resume.pdf \
  https://startupkit.app/api/v1/applications/app_jkl012/resume

Response:

  • 302 Found – Redirect to signed download URL. Use -L (follow redirects) with curl.
  • 403 Forbidden – Token lacks applications:read or candidates:read scope.
  • 404 Not Found – Application not found, or no resume attached.

Notes:

  • Signed URLs expire after 30 minutes. If expired, request a new one.
  • The Content-Disposition header is set to attachment (triggers browser download).
  • Resume file types: PDF, DOC, DOCX (as uploaded by the candidate).

Candidate PII Redaction

When a token has applications:read but not candidates:read, candidate data in application responses is redacted to only the ID:

{
  "candidate": {
    "id": "cand_ghi789"
  }
}

No first_name, last_name, or email fields are included. The metafields array and the resume metadata fields are omitted entirely under the same rule, since they can carry candidate PII too. This lets job board integrations track application status without exposing candidate contact information.


Troubleshooting

Authentication Issues

401 Unauthorized

If you’re getting a 401 response, check:

  1. Token format: Ensure the token matches the value shown once at creation (24-character base58 string). If you no longer have it, revoke the token and create a new one — Kit stores only a hashed digest and cannot show it again.
  2. Authorization header: Must be Authorization: Bearer TOKEN or Authorization: token TOKEN
  3. Token still exists: Confirm the token is still listed in Settings > API — a revoked token is deleted, not disabled
  4. Account membership: Verify the token owner is still a member of the account

403 Forbidden

When you get a 403, the token is valid but lacks required scope:

  1. Check required scopes: Each endpoint documents its required scope (e.g., job_postings:read)
  2. Add missing scopes: Edit the token in Settings > API and add the needed scope
  3. Scope format: Must match exactly — job_postings:read, not job_postings or read

Data Access Issues

Empty data array in responses

If the API returns {"data": [], "pagination": {...}} but you expect results:

  1. Account isolation: API tokens only see data from their bound account
  2. Verify account: Check which account the token belongs to in Settings > API
  3. Cross-account access: Tokens cannot access data from other accounts, even if the user is a member

Candidate PII is redacted

If you see only {"candidate": {"id": "cand_..."}} without name or email:

  1. Expected behavior: This happens when the token has applications:read but not candidates:read
  2. Add scope: To see full candidate details, add the candidates:read scope to your token
  3. Privacy by design: This lets integrations track applications without exposing contact info

metafields is missing from application responses

The metafields array is gated on candidates:read, not applications:read, because metafield values can hold AI-extracted candidate PII. If responses have no metafields key, the token is missing candidates:read. Add that scope only if the integration should also see candidate names, emails, and resumes — the two travel together.

Query and Filtering Issues

404 when looking up specific records

  1. Use prefix IDs: Job postings use job_abc123 format, not database IDs like 42
  2. Case sensitivity: Prefix IDs are case-sensitive
  3. Account scoping: The resource must belong to the token’s account

Filter parameters not working

  1. Valid status values:
    • Job postings: draft, published, paused, closed (case-sensitive)
    • Applications: active, rejected (case-sensitive)
  2. Parameter format: Use ?status=published, not ?status=Published or ?filter[status]=published

Frequently Asked Questions

How does pagination work?

All list endpoints return 20 items per page (fixed). Use ?page=2 for the next page. The pagination object shows total_pages and total_count.

Do tokens expire automatically?

No. There is no expiration setting when you create a token, so a token stays valid until:

  • Manually revoked in Settings > API
  • User is removed from the account
  • Account is deleted

Should I use one token for multiple integrations?

No. Best practice is one token per integration with minimal scopes. This makes it easier to:

  • Audit which integration is making requests (via last_used_at)
  • Revoke access to a specific integration without affecting others
  • Grant different permissions to different systems

How do webhooks relate to the API?

Webhooks notify you of events (new application, stage change). The API lets you query current state. Use webhooks to trigger your system, then use the API to fetch full details.

Can I get more than 20 items per page?

No. The page size is fixed at 20 items to ensure consistent performance. Use the page parameter to iterate through all results.

Quick Checklist

  • Create an API token at Settings > API — each token is bound to that account
  • Assign only the scopes your integration needs (best practice: one token per integration with minimal scopes)
  • Withhold candidates:read unless the integration should see candidate PII — that scope also controls the metafields array and resume access
  • Include Authorization: Bearer TOKEN on every request
  • Use prefix IDs (e.g., job_abc123) in URLs, not database IDs
  • Handle 400 and 422 responses for validation errors
  • Handle 401 responses — your token may have been revoked
  • Handle 403 responses — your token may lack a required scope
  • Use page parameter to paginate through large result sets (20 items per page)

Type to search...