For IT and engineering teams. The REST API is the organization-level integration, authenticated with a key issued to your company. If you want to work with your own calls from an AI assistant, use the MCP connector instead; it needs no code.
The Junior REST API returns call (interview) records and their transcripts. Where write access is enabled for your organization, it also creates projects and ingests calls from sources Junior has no direct integration with.
The Junior team switches the REST API on for each organization, and write access separately. Contact your Junior representative to arrange either.
Base URL:
https://api.junior.ai/v1
Authentication
Every request needs an API key, issued to your organization by the Junior team and sent as a bearer token. Keys start with jr_live_:
curl https://api.junior.ai/v1/calls \
-H "Authorization: Bearer $JUNIOR_API_KEY"Keys are shown once at issuance, so store them in your secret manager. A missing or invalid key returns 401 with code unauthorized.
Your first request
List calls from a date range. start_date and end_date are both required; the endpoint returns 400 without them. Each is an RFC 3339 date-time, and both UTC (...Z) and a numeric offset (...-05:00) are accepted:
curl -G "https://api.junior.ai/v1/calls" \
--data-urlencode "start_date=2026-01-01T00:00:00Z" \
--data-urlencode "end_date=2026-12-31T23:59:59Z" \
--data-urlencode "per_page=25" \
-H "Authorization: Bearer $JUNIOR_API_KEY"Then fetch one call in full, using a call_id from that response:
curl "https://api.junior.ai/v1/calls/$CALL_ID" \
-H "Authorization: Bearer $JUNIOR_API_KEY"Creating a project
POST /v1/projects creates a project and its lead and member users, given their email addresses. Projects and billing offices are part of the write access, so these endpoints return 403 with write_not_enabled until it's on.
curl -X POST "https://api.junior.ai/v1/projects" \
-H "Authorization: Bearer $JUNIOR_API_KEY" \
-H "content-type: application/json" \
-d '{
"name": "Project Atlas",
"lead_emails": ["lead@yourfirm.com"],
"member_emails": ["analyst@yourfirm.com"],
"billing_code": "ATLAS-2026"
}'Only name and at least one lead_emails entry are required. billing_office_name takes an office name rather than an id; GET /v1/billing-offices lists the valid ones.
People can join before or after project creation. Existing users in your organization are added immediately. Addresses without an account receive a pending project invitation, without an invitation email; they are added automatically when they sign in. Pending invitations expire after one year. The response separates current lead_emails and member_emails from pending invited_lead_emails and invited_member_emails. Compare both sets against your request to see who has access now and who is still pending.
The first address in lead_emails is the designated project creator. If that person has not signed in yet, creator attribution stays empty until their lead membership is granted. A lead who already belongs only to another organization rejects the entire request with 400; members who belong only to another organization are skipped.
Retrying is safe
Send the same body twice within 24 hours and the second request comes back with the first one's 201 and the same project, rather than a duplicate. You don't have to opt in: Junior fingerprints the request body, so a timeout you retry blindly can't leave you with two projects. This matters because there is no project delete in this API; a duplicate needs the Junior team to clean it up by hand.
To create two projects with identical bodies deliberately, distinguish them with an Idempotency-Key header:
curl -X POST "https://api.junior.ai/v1/projects" \
-H "Authorization: Bearer $JUNIOR_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "content-type: application/json" \
-d '{ "name": "Project Atlas", "lead_emails": ["lead@yourfirm.com"] }'Two different keys mean two intended creates. A key you supply also keeps replaying after 24 hours (it's kept for at least seven days), so it's the right tool if a retry might arrive days late. Reusing one key with a changed body returns 422 rather than guessing which you meant; retrying while the first attempt is still running returns 409.
Ingesting a call
If you have transcripts in a system Junior doesn't integrate with directly, POST /v1/calls files them as real calls. They run the same processing pipeline as an upload through the app, so they end up cleaned, summarized into Key Takeaways, and searchable.
This requires write access for your organization; without it the request returns 403 with code write_not_enabled.
A call must arrive with project_id or at least one attendee_emails entry. An API key authenticates your organization, not a person, so an ingested call has no creator. With no project and no attendees, nobody in Junior would be able to see it, so the request is rejected with 400. Use project_id to file it in a project, where every member of that project sees it, or attendee_emails so the people who were on the call can find it.
Project ids come from GET /v1/projects. Its name filter matches the exact name, ignoring case:
curl -G "https://api.junior.ai/v1/projects" \
--data-urlencode "name=Project Atlas" \
-H "Authorization: Bearer $JUNIOR_API_KEY"Then ingest:
curl -X POST "https://api.junior.ai/v1/calls" \
-H "Authorization: Bearer $JUNIOR_API_KEY" \
-H "content-type: application/json" \
-d '{
"external_id": "your-system-call-4821",
"project_id": "'"$PROJECT_ID"'",
"name": "Jane Expert",
"job_title": "VP Engineering",
"expert_organization": "AcmeCo",
"date": "2026-01-02T10:00:00-05:00",
"source": "YourSystem",
"transcript": "Speaker 1: ...\nSpeaker 2: ..."
}'Only name and transcript are required beyond the rule above.
Send external_id. It's your own identifier for the call, and it's the deduplication key: re-sending the same one is a no-op, which is what makes retries safe. Omit it and Junior derives a fingerprint from the call's contents, so a corrected re-send lands as a brand-new call.
The response is 202:
{
"data": {
"external_id": "your-system-call-4821",
"project_id": "…",
"status": "processing"
}
}The call is created asynchronously and takes a minute or two to appear. Poll GET /v1/calls to pick it up; it won't come back in this response.
Transcripts are plain text or markdown; this is not a file upload. Very large ones return 413. The limit applies to the serialized request, so escape-heavy text (lots of newlines or quotes) fits less than its raw length suggests.
Pagination
List endpoints are page-based. Pass page (starting at 1) and per_page (25 by default, up to 100), and read the pagination object to decide whether to keep going:
{
"data": [{ "call_id": "..." }],
"pagination": {
"page": 1,
"per_page": 25,
"total_items": 412,
"total_pages": 17
}
}Keep requesting until page equals total_pages. List responses return call references; fetch each call by its call_id (as in your first request) for the full record, including the transcript.
Errors
Every error uses the same envelope:
{
"error": {
"code": "bad_request",
"message": "Human-readable explanation",
"request_id": "01HQ..."
}
}code is one of bad_request, unauthorized, forbidden, rest_not_enabled, write_not_enabled, write_not_configured, not_found, payload_too_large, idempotency_key_in_progress, idempotency_key_reused, rate_limited, or internal_error. Log request_id: it's the fastest way for Junior support to trace a specific failed call.
Two 403 codes are easy to mistake for a bad key. rest_not_enabled means
the REST API isn't switched on for your organization yet. forbidden means
the key is valid but you're calling from an IP outside its allowlist. Neither
is fixed by reissuing the key.
Rate limits
Each API key has a per-minute limit, set for your organization. Responses carry a RateLimit header with the limit, the remaining requests, and the seconds until reset, plus a RateLimit-Policy header. On 429 (rate_limited), wait the number of seconds in Retry-After before trying again.
Field availability
Some fields on a call depend on your organization's configuration. For example, participant emails are present only when your organization has enabled that field. A field that isn't enabled comes back omitted or null, so treat these as optional in your client.
Full reference
See the API reference for every endpoint, parameter, and response field.