Juniob Partner API
Integrate Juniob developer reports directly into your platform. Sign a JWT on your server, make a REST call, get structured hiring data back.
https://api.juniob.io/partnerWhat Is the Partner API?
The Juniob Partner API gives trusted third-party platforms secure access to developer assessment reports. Whether you are building an ATS, a training platform, or a job board, you can show Juniob hiring insights inside your product with minimal integration effort.
Principles
- Security by design. Every request is signed with a partner secret. Tokens are short-lived and generated server-side.
- Developer consent. Data is only shared for developers who have connected their Juniob account to your platform.
- Minimal surface area. Use the
sectionsquery param to fetch only what you need. - No session overhead. No OAuth, no refresh tokens. Sign a JWT and call the API.
What You Can Do
- Fetch assessment reports with selective sections
- Generate signed view URLs so recruiters can open the full report in a tab
JWT Bearer Auth
The endpoints /developer and /generate-redirection-url require a Bearer JWT signed with your partner secret, provided by Juniob. Always sign tokens on your backend - never expose your secret in client code.
Authorization: Bearer <your-signed-jwt>
JWT Structure
Your token payload must include these three fields:
| Field | Type | Required | Description |
|---|---|---|---|
org | string | Yes | Your organization name as registered with Juniob. |
developerEmail | string | Yes | Email of the developer. Must match their Juniob account email (case-insensitive). |
externalUserId | string | number | Yes | ID of the requesting user on your platform (e.g. recruiter ID). Used for billing deduplication. |
const jwt = require("jsonwebtoken"); const token = jwt.sign( { org: "acme-corp", developerEmail: "[email protected]", externalUserId: "recruiter-42", }, process.env.JUNIOB_PARTNER_SECRET, { expiresIn: "10m" } );
GET /partner/developer
Retrieve a structured assessment report for the developer identified in the JWT. Use the sections query parameter to fetch only what you need.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
sections | string | No | Comma-separated section names: profile, summary, projects, technical, risk, interview, growth. Omit to return all seven sections. |
Example Request
const res = await fetch( "https://api.juniob.io/partner/developer?sections=profile,summary", { headers: { Authorization: `Bearer ${token}` } } ); const data = await res.json();
Response - ASSESSED
{ "status": "ASSESSED", "profile": { "email": "[email protected]", "firstName": "Sarah", "lastName": "Anderson", "location": "Paris, France", "developerType": "FULLSTACK", // FRONTEND | BACKEND | FULLSTACK | MOBILE "learningBackground": "BOOTCAMP", // CS_DEGREE | RELATED_DEGREE | BOOTCAMP | SELF_TAUGHT | CAREER_CHANGE "totalCodingMonths": 18, "techExperiences": [ { "stackName": "React", "months": 14 }, { "stackName": "TypeScript", "months": 10 } ] }, "summary": { "recommendation": "SAFE_TO_INTERVIEW", "recommendationReasons": ["Clean architecture", "TypeScript discipline"], "overallScore": 74, "juniorLevel": "ABOVE_EXPECTED", "juniorLevelContext": "Performs well above average junior level", "conclusion": "Strong candidate for a mid-level track...", "timeToProductivity": "2-4 weeks", "keyInsights": [...] }, }
Response - NOT_ASSESSED
status tells you the report is not ready. assessmentStatus tells you where the developer currently is in the Juniob pipeline, so you can decide whether to retry later or consider them unready.
{ "status": "NOT_ASSESSED", "assessmentStatus": "ANALYZING" // assessmentStatus possible values: // REGISTERING — profile incomplete, not yet submitted // PROJECTS_SUBMITTED — submitted, analysis not started yet // ANALYZING — analysis in progress // PENDING_ANALYSIS — analysis needs to be re-run (project was modified) }
HTTP 204 No Content. No billing is applied for 204 or NOT_ASSESSED responses.GET /partner/generate-redirection-url
Generate a signed URL that opens the full Juniob developer report in a browser tab. The token is valid for 7 days and is scoped to your partner app's secret - it can be safely sent in emails or stored in your system.
Example Request
const res = await fetch( "https://api.juniob.io/partner/generate-redirection-url", { headers: { Authorization: `Bearer ${token}` } } ); const url = await res.json(); // "https://juniob.io/partner/view?accessViewToken=eyJ..." window.open(url, "_blank");
Response
"https://juniob.io/partner/view?accessViewToken=eyJhbGciOiJIUzI1NiJ9..."org, developerId, and externalUserId (carried from your original JWT). This means billing deduplication works correctly even if the recruiter uses both the API pull and the redirect link.Available Sections
Pass any combination of these section names in the sections query parameter on /partner/developer. Omit the parameter entirely to receive all seven sections. Sections not requested are returned as null.
Response Values
profile - PartnerProfileDto
| Field | Type | Description |
|---|---|---|
email | string | Developer email address |
firstName | string? | First name |
lastName | string? | Last name |
location | string? | Self-reported location |
developerType | string? | FRONTEND | BACKEND | FULLSTACK | MOBILE |
learningBackground | string? | CS_DEGREE | RELATED_DEGREE | BOOTCAMP | SELF_TAUGHT | CAREER_CHANGE |
totalCodingMonths | number? | Total months of coding experience |
techExperiences | { stackName, months }[] | Technologies with experience duration |
summary - PartnerSummaryDto
| Field | Type | Description |
|---|---|---|
recommendation | string | Hiring recommendation text |
recommendationReasons | string[] | Bullet-point reasons |
overallScore | number | 0–100 assessment score |
juniorLevel | string | ABOVE_EXPECTED | WITHIN_EXPECTED | BELOW_EXPECTED |
juniorLevelContext | string? | Narrative explanation of the level |
keyInsights | unknown | Top insights about the candidate |
conclusion | string | Free-text hiring conclusion |
timeToProductivity | string? | Estimated ramp-up time, e.g. "2-4 weeks" |
projects - PartnerProjectDto[]
| Field | Type | Description |
|---|---|---|
name | string | Project name |
projectType | string | Type of project |
techStack | string[] | Technologies used |
score | number | Project quality score |
strengths | string[] | Identified strengths |
areasForImprovement | string[] | Areas needing improvement |
bestPractices | string[] | Best practices observed |
commitAnalysis | PartnerCommitAnalysisDto? | Commit discipline, authenticity impact, observations |
technical - PartnerTechnicalDto
| Field | Type | Description |
|---|---|---|
technicalBreakdown | unknown | Detailed breakdown by technical category |
techProficiency | Record<string, number>? | Stack → proficiency score map, e.g. { React: 8, TypeScript: 7 } |
risk - PartnerRiskDto
| Field | Type | Description |
|---|---|---|
authenticityAnalysis | unknown | Whether the work appears genuine |
riskFlags | string[] | Minor risk indicators |
redFlags | string[] | Significant concerns requiring follow-up |
commitHistoryAggregate | unknown | Aggregated commit stats across all analyzed projects |
interview - PartnerInterviewDto
| Field | Type | Description |
|---|---|---|
interviewQuestions | string[] | Tailored questions based on the developer's projects |
growth - PartnerGrowthDto
| Field | Type | Description |
|---|---|---|
onboardingTimeline | unknown | Structured onboarding plan |
trainingPriorities | string[] | Key skills to develop first |
mentoringNeeds | string[] | Areas where mentoring is recommended |
growthPotential | string? | Assessment of long-term growth potential |
Billing
Juniob bills per unique recruiter × developer pair, identified by externalUserId × developerId. Access is logged on the first successful retrieval of an ASSESSED report. Subsequent calls for the same pair - whether via API pull or redirect link - are free.
| Trigger | Result | Billed? |
|---|---|---|
/developer → NOT_ASSESSED | No report available | No |
/developer → ASSESSED | Report delivered | Yes - once |
| Same recruiter: API pull + redirect link | Deduplication via upsert | Still once |
externalUserId values for your recruiters to ensure correct deduplication across both integration patterns.Error Handling
| Status | Scenario | Body |
|---|---|---|
| 401 | Missing Authorization header | Missing token |
| 401 | JWT cannot be decoded | Invalid token format |
| 401 | Signature invalid or token expired | Invalid or expired token |
| 403 | org not found or partner not approved | Unauthorized partner |
| 403 | Developer profile set to private | Developer profile not visible |
| 204 | Developer email not found in Juniob | empty body |
| 200 | Developer not yet assessed | { status: 'NOT_ASSESSED', assessmentStatus: 'ANALYZING' | ... } |
| 200 | Success | { status: 'ASSESSED', ...sections } |
Node.js
Full integration example using jsonwebtoken and the native fetch API (Node 18+).
import jwt from "jsonwebtoken"; const BASE = "https://api.juniob.io/partner"; const ORG = process.env.JUNIOB_ORG; const SECRET = process.env.JUNIOB_PARTNER_SECRET; function signToken(developerEmail, externalUserId) { return jwt.sign( { org: ORG, developerEmail, externalUserId }, SECRET, { expiresIn: "10m" } ); } // ── Pull a selective report ────────────────────────────────────────────────── async function getDeveloperReport(developerEmail, recruiterId, sections = []) { const token = signToken(developerEmail, recruiterId); const params = sections.length ? `?sections=${sections.join(",")}` : ""; const res = await fetch(`${BASE}/developer${params}`, { headers: { Authorization: `Bearer ${token}` }, }); if (res.status === 204) return null; // developer not in Juniob return res.json(); // { status, profile?, summary?, ... } } // ── Generate a recruiter view URL ──────────────────────────────────────────── async function getViewUrl(developerEmail, recruiterId) { const token = signToken(developerEmail, recruiterId); const res = await fetch(`${BASE}/generate-redirection-url`, { headers: { Authorization: `Bearer ${token}` }, }); return res.json(); // "https://juniob.io/partner/view?accessViewToken=..." } // Usage const report = await getDeveloperReport( "[email protected]", "recruiter-42", ["profile", "summary", "risk"] ); if (report?.status === "ASSESSED") { console.log(report.summary.recommendation); // "SAFE_TO_INTERVIEW" console.log(report.summary.overallScore); // 74 console.log(report.risk.redFlags); // [] }
Python
Full integration example using PyJWT and requests.
import os import jwt import requests from datetime import datetime, timedelta, timezone BASE = "https://api.juniob.io/partner" ORG = os.environ["JUNIOB_ORG"] SECRET = os.environ["JUNIOB_PARTNER_SECRET"] def sign_token(developer_email: str, external_user_id: str) -> str: payload = { "org": ORG, "developerEmail": developer_email, "externalUserId": external_user_id, "exp": datetime.now(timezone.utc) + timedelta(minutes=10), } return jwt.encode(payload, SECRET, algorithm="HS256") def get_developer_report( developer_email: str, recruiter_id: str, sections: list[str] = [], ) -> dict | None: token = sign_token(developer_email, recruiter_id) params = {"sections": ",".join(sections)} if sections else {} resp = requests.get( f"{BASE}/developer", headers={"Authorization": f"Bearer {token}"}, params=params, ) if resp.status_code == 204: return None # developer not in Juniob resp.raise_for_status() return resp.json() def get_view_url(developer_email: str, recruiter_id: str) -> str: token = sign_token(developer_email, recruiter_id) resp = requests.get( f"{BASE}/generate-redirection-url", headers={"Authorization": f"Bearer {token}"}, ) resp.raise_for_status() return resp.json() # Usage report = get_developer_report( "[email protected]", "recruiter-42", ["profile", "summary", "risk"], ) if report and report["status"] == "ASSESSED": print(report["summary"]["recommendation"]) # "SAFE_TO_INTERVIEW" print(report["summary"]["overallScore"]) # 74 print(report["risk"]["redFlags"]) # []
Ready to integrate?
Request your partner signing secret and go live in minutes.