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 lacksapplications:readorcandidates:readscope. -
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-Dispositionheader is set toattachment(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:
- 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.
-
Authorization header: Must be
Authorization: Bearer TOKENorAuthorization: token TOKEN - Token still exists: Confirm the token is still listed in Settings > API — a revoked token is deleted, not disabled
- 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:
-
Check required scopes: Each endpoint documents its required scope (e.g.,
job_postings:read) - Add missing scopes: Edit the token in Settings > API and add the needed scope
-
Scope format: Must match exactly —
job_postings:read, notjob_postingsorread
Data Access Issues
Empty data array in responses
If the API returns {"data": [], "pagination": {...}} but you expect results:
- Account isolation: API tokens only see data from their bound account
- Verify account: Check which account the token belongs to in Settings > API
- 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:
-
Expected behavior: This happens when the token has
applications:readbut notcandidates:read -
Add scope: To see full candidate details, add the
candidates:readscope to your token - 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
-
Use prefix IDs: Job postings use
job_abc123format, not database IDs like42 - Case sensitivity: Prefix IDs are case-sensitive
- Account scoping: The resource must belong to the token’s account
Filter parameters not working
-
Valid status values:
- Job postings:
draft,published,paused,closed(case-sensitive) - Applications:
active,rejected(case-sensitive)
- Job postings:
-
Parameter format: Use
?status=published, not?status=Publishedor?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:readunless the integration should see candidate PII — that scope also controls themetafieldsarray and resume access -
Include
Authorization: Bearer TOKENon every request -
Use prefix IDs (e.g.,
job_abc123) in URLs, not database IDs -
Handle
400and422responses for validation errors -
Handle
401responses — your token may have been revoked -
Handle
403responses — your token may lack a required scope -
Use
pageparameter to paginate through large result sets (20 items per page)