Developer documentation
The Assessly API, and the webhooks that call you back
Read submissions and assessments from your own system, and be told the moment a candidate finishes. Keys are scoped, requests are rate limited by plan, and every webhook is signed.
Paid plans only
API access is not available on the Free plan. Starter and above include it, with a request allowance that grows with the plan.
curl -H "Authorization: Bearer sk_live_..." \ https://assessly.app/api/v1/submissions
Two halves
REST API
Pull what the dashboard shows: every submission with its answers and scores, every assessment with its questions.
- List and filter submissions
- Read one submission in full
- List and read assessments
- Keys scoped per integration
Webhooks
Be told when a submission is created, updated, completed or scored, instead of asking.
- Four events
- Retried on failure
- HMAC-SHA256 signed
- Filter by event when you register
Three steps to a first response
Create a key
In Settings, Integrations. It is shown once; copy it then.
sk_live_<random_string>
Make a request
Send the key as a bearer token on any endpoint.
GET /api/v1/submissions Authorization: Bearer sk_live_...
Register a webhook
Give us a URL and pick the events. We call you from then on.
POST https://your-app.example/webhooks/assessly
Authentication
One header on every request. A key belongs to a company and carries the scopes it was created with.
Keys look like sk_live_<random_string>. Send yours in the Authorization header, as a bearer token.
const response = await fetch('https://assessly.app/api/v1/submissions?assessment_id=abc123&page=1&limit=50', {method: 'GET',headers: {'Authorization': 'Bearer sk_live_abc123...','Content-Type': 'application/json'}});const data = await response.json();console.log(data);
Scopes
A new key starts with the first and nothing else. Add only what the integration will call.
| Scope | Allows |
|---|---|
| submissions.read | Read submission data (default) |
| submissions.write | Create and update submissions |
| assessments.read | Read assessment data and configurations |
| assessments.write | Create, update, and delete assessments |
| webhooks.read | View webhook configurations |
| webhooks.write | Create and manage webhooks |
Rate limits
Requests per hour, per company. API access is not available on the Free plan.
| Plan | Requests per hour |
|---|---|
| Starter | 500 |
| Pro | 2,000 |
| Business | 5,000 |
| Enterprise | Custom |
Reference
Open an endpoint for its parameters, a request in five languages, and the shape of the response.
Submissions
GET/api/v1/submissionsList all submissions for your company with optional filtering and pagination
Query parameters
| Query parameter | Type | What it does |
|---|---|---|
| assessment_id | string | Only submissions to this assessment |
| status | string | completed or pending |
| candidate_name | string | Partial match on the candidate name |
| candidate_email | string | Partial match on the candidate email |
| min_score | number | Score at or above this value |
| max_score | number | Score at or below this value |
| submitted_after | string | Submitted after this date (ISO 8601) |
| submitted_before | string | Submitted before this date (ISO 8601) |
| has_ai_summary | boolean | Whether an AI summary exists |
| sort_by | string | submitted_at, score, time_spent_seconds or status |
| sort_order | string | asc or desc. Default: desc |
| page | number | Page number. Default: 1 |
| limit | number | Items per page. Default: 50, maximum: 100 |
Request
const response = await fetch('https://assessly.app/api/v1/submissions?assessment_id=abc123&status=completed&min_score=70&submitted_after=2024-01-01T00%3A00%3A00Z&has_ai_summary=true&sort_by=score&sort_order=desc&page=1&limit=50', {method: 'GET',headers: {'Authorization': 'Bearer sk_live_abc123...','Content-Type': 'application/json'}});const data = await response.json();console.log(data);
Response
{"data": [{"id": "sub_123","assessment_id": "abc123","candidate_name": "John Doe","candidate_email": "john@example.com","status": "completed","score": 85,"submitted_at": "2024-11-24T10:30:00Z","created_at": "2024-11-24T10:00:00Z","custom_fields": {"source": "linkedin","campaign_id": "summer2024","referrer": "john_doe"}}],"pagination": {"page": 1,"limit": 50,"total": 150,"total_pages": 3}}
GET/api/v1/submissions/:idRetrieve detailed information about a specific submission, including all responses and AI analysis
Path parameters
| Path parameter | Type | What it does |
|---|---|---|
| id | string | The submission id |
Request
const response = await fetch('https://assessly.app/api/v1/submissions/sub_123', {method: 'GET',headers: {'Authorization': 'Bearer sk_live_abc123...','Content-Type': 'application/json'}});const data = await response.json();console.log(data);
Response
{"id": "sub_123","assessment_id": "abc123","assessment_title": "Frontend Developer Assessment","candidate_info": {"name": "John Doe","email": "john@example.com","phone": "+1234567890"},"responses": [{"question_id": "q_001","question_text": "Describe your experience with React","question_type": "text","answer": "I have 3 years of experience...","score": 90,"ai_feedback": "Excellent answer demonstrating deep understanding."}],"submitted_at": "2024-11-24T10:30:00Z","status": "completed","score": 85,"grade": "A","ai_summary": {"overall": "Strong candidate with excellent technical skills...","strengths": ["Problem solving","Communication","Technical knowledge"],"concerns": []},"time_spent_seconds": 1800,"created_at": "2024-11-24T10:00:00Z","custom_fields": {"source": "linkedin","campaign_id": "summer2024","referrer": "john_doe","ats_candidate_id": "12345"}}
Custom fields from the link
Anything you append to an assessment link comes back on the submission. Track the source, the campaign, or the candidate id in your ATS.
Every query parameter on the link, other than the reserved ones, is stored in custom_fields when the candidate submits.
https://assessly.app/a/company/assessment?source=linkedin&campaign_id=summer2024&referrer=john_doeReserved
The link keeps these for itself; they are not stored.
tokenassessmentIdassessment_idcompanySlugassessmentSlugcompany-slugassessment-slug
Common uses
- Marketing attribution
- ?source=linkedin&campaign_id=123
- ATS integration
- ?ats_candidate_id=12345
- Referral tracking
- ?referrer=john_doe
- Location
- ?location=sf&office=hq
In the response
{"id": "sub_123","assessment_id": "abc123","candidate_name": "John Doe","custom_fields": {"source": "linkedin","campaign_id": "summer2024","referrer": "john_doe","ats_candidate_id": "12345"}}
Assessments
GET/api/v1/assessmentsList all assessments for your company with optional filtering and pagination
Query parameters
| Query parameter | Type | What it does |
|---|---|---|
| status | string | active, paused or archived |
| search | string | Matches the title and the description |
| is_public | boolean | Whether the assessment is public |
| created_after | string | Created after this date (ISO 8601) |
| created_before | string | Created before this date (ISO 8601) |
| has_time_limit | boolean | Whether a time limit is set |
| has_max_submissions | boolean | Whether a submission cap is set |
| has_expiry_date | boolean | Whether an expiry date is set |
| include_counts | boolean | Add submission counts to each row |
| sort_by | string | created_at, updated_at, title or status |
| sort_order | string | asc or desc. Default: desc |
| page | number | Page number. Default: 1 |
| limit | number | Items per page. Default: 50, maximum: 100 |
Request
const response = await fetch('https://assessly.app/api/v1/assessments?status=active&search=frontend&is_public=true&has_time_limit=true&include_counts=true&sort_by=created_at&sort_order=desc&page=1&limit=50', {method: 'GET',headers: {'Authorization': 'Bearer sk_live_abc123...','Content-Type': 'application/json'}});const data = await response.json();console.log(data);
Response
{"data": [{"id": "abc123","title": "Frontend Developer Assessment","slug": "frontend-developer-assessment","description": "Assess frontend development skills","status": "active","is_public": true,"created_at": "2024-11-01T10:00:00Z","updated_at": "2024-11-24T10:00:00Z","expires_at": null,"time_limit_minutes": 60,"max_submissions": 100,"max_attempts": 2,"show_timer": true,"assessment_format": "standard","submission_count": 45}],"pagination": {"page": 1,"limit": 50,"total": 12,"total_pages": 1}}
GET/api/v1/assessments/:idRetrieve detailed information about a specific assessment, including all questions and configuration
Path parameters
| Path parameter | Type | What it does |
|---|---|---|
| id | string | The assessment id |
Request
const response = await fetch('https://assessly.app/api/v1/assessments/abc123', {method: 'GET',headers: {'Authorization': 'Bearer sk_live_abc123...','Content-Type': 'application/json'}});const data = await response.json();console.log(data);
Response
{"id": "abc123","title": "Frontend Developer Assessment","description": "Assess frontend development skills including React, TypeScript, and CSS","instructions": "Complete all questions within 60 minutes","status": "active","password_protected": false,"time_limit_minutes": 60,"personal_info_fields": ["name","email","phone"],"questions": [{"id": "q_001","type": "text","question": "Describe your experience with React","description": "Please provide details about your React experience","required": true,"order": 1,"validation_rules": {"min_length": 50,"max_length": 1000}},{"id": "q_002","type": "multiple_choice","question": "Which React hooks have you used?","options": ["useState","useEffect","useContext","useReducer"],"required": true,"order": 2}],"created_at": "2024-11-01T10:00:00Z","updated_at": "2024-11-24T10:00:00Z","total_submissions": 45}
PATCH/api/v1/assessments/:idUpdate assessment details, questions, and configuration programmaticallyNot yet available
Not served yet. Update assessment details, questions, and configuration programmatically. It will appear here when it ships, and the change log will say so.
DELETE/api/v1/assessments/:idPermanently remove an assessment from your accountNot yet available
Not served yet. Permanently remove an assessment from your account. It will appear here when it ships, and the change log will say so.
Webhooks
Register a URL in Settings, Integrations, choose the events, and each one arrives as a POST with a signature to check.
Events
submission.createdTriggered when a new submission is created
submission.updatedTriggered when a submission is updated
submission.completedTriggered when a submission is completed
submission.scoredTriggered when AI scoring is finished
Payload
{"event": "submission.created","timestamp": "2024-11-24T10:30:00Z","data": {"id": "sub_123","assessment_id": "abc123","candidate_name": "John Doe","candidate_email": "john@example.com","status": "completed","submitted_at": "2024-11-24T10:30:00Z"}}
Verify the signature
Every delivery carries X-Webhook-Signature, an HMAC-SHA256 of the raw body with your webhook secret. Compare it in constant time before you trust the event.
const crypto = require('crypto');const express = require('express');const app = express();app.use(express.raw({ type: 'application/json' }));function verifyWebhookSignature(payload, signature, secret) {const hmac = crypto.createHmac('sha256', secret);const digest = hmac.update(payload).digest('hex');return crypto.timingSafeEqual(Buffer.from(signature),Buffer.from(digest));}app.post('/webhooks/assessly', (req, res) => {const signature = req.headers['x-webhook-signature'];const secret = process.env.WEBHOOK_SECRET;if (!verifyWebhookSignature(req.body, signature, secret)) {return res.status(401).send('Invalid signature');}const event = JSON.parse(req.body.toString());switch (event.event) {case 'submission.created':console.log('New submission:', event.data);break;case 'submission.completed':console.log('Submission completed:', event.data);break;}res.status(200).send('OK');});app.listen(3000);
Ready to build?
Create a key on any paid plan and the first request works in a minute. If something in this reference is wrong or missing, say so.