REST API
API Documentation
Integrate EditingTests.com into your ATS, HRIS, or HR platform. Invite candidates, retrieve results, and automate your editorial hiring workflow.
https://editingtests.com/api/v1
v1 (stable)
JSON
Authentication
All API requests require a Bearer token passed in the Authorization header. You can generate and rotate your API key from your client dashboard under Dashboard → API tokens.
Request header
Authorization: Bearer et_live_xxxxxxxxxxxxxxxxxxxx Content-Type: application/json
Keep your key secret. Never expose it in client-side code or public repositories. Use environment variables. Contact [email protected] to request API access.
Rate Limits
The API enforces per-account rate limits to ensure fair usage. Rate limit headers are returned with every response.
| Endpoint | Limit | Window |
|---|---|---|
| POST /invitations | 60 requests | per minute, per account |
| GET /invitations, GET /results | 120 requests | per minute, per account |
| All endpoints | 2,000 requests | per hour, per account |
| All endpoints | 60 requests | per minute, per IP address |
Rate limit response headers
X-RateLimit-Limit: 60 X-RateLimit-Remaining: 47 X-RateLimit-Reset: 1714000000
Error Handling
All errors return a JSON body with a code and human-readable message.
Error response shape
{
"error": {
"code": "insufficient_credits",
"message": "Your account has 0 credits. Purchase credits to send invitations.",
"docs": "https://editingtests.com/api-docs#errors"
}
}
| HTTP Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | Missing or malformed parameters |
| 401 | unauthorized | Invalid or missing API key |
| 402 | insufficient_credits | Account has no credits |
| 404 | not_found | Resource does not exist |
| 409 | already_submitted | Candidate has already completed this invitation |
| 422 | validation_error | Parameter validation failed — see errors array |
| 429 | rate_limited | Too many requests — wait for X-RateLimit-Reset |
| 500 | server_error | Internal error — contact support |
/api/v1/tests
Returns the full list of available assessments with slugs, durations, credit costs, and descriptions. Use the slug when creating invitations.
Example request
curl https://editingtests.com/api/v1/tests \ -H "Authorization: Bearer et_live_xxxx"
Example response 200 OK
{
"object": "list",
"data": [
{
"slug": "grammar-test",
"name": "Grammar Test",
"credits": 1,
"duration_minutes": 15
},
{
"slug": "editing-test",
"name": "Editing Test",
"credits": 1,
"duration_minutes": 15
},
{
"slug": "full-editing-assessment",
"name": "Full Editing Assessment",
"credits": 2,
"duration_minutes": 45
}
// ... all 7 tests
]
}
/api/v1/invitations
Creates and sends a test invitation to a candidate. One credit is deducted per invitation, or two if the request includes the Full Editing Assessment. A multi-test invitation costs one credit however many component tests it contains. Returns the invitation object including a direct test URL.
Retries are safe. Send an Idempotency-Key header, or a unique reference, and a repeated request returns the original invitation with an Idempotent-Replay: true header instead of spending a second credit and emailing the candidate twice.
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
| candidate_email | string | Required | Candidate's email address |
| candidate_name | string | Required | Candidate's full name |
| tests | array | Required | Array of test slugs (e.g. ["grammar-test", "editing-test"]) |
| expiry_hours | integer | Optional | Hours until link expires. Default: 120 (5 days). Min: 24, Max: 720 |
| custom_message | string | Optional | Optional message included in the invitation email (max 500 chars) |
| reference | string | Optional | Your internal reference (job ID, ATS ID, etc.) — returned in results |
| send_email | boolean | Optional | Send invitation email. Default: true. Set false to return URL only |
Example request
curl -X POST https://editingtests.com/api/v1/invitations \
-H "Authorization: Bearer et_live_xxxx" \
-H "Content-Type: application/json" \
-d '{
"candidate_email": "[email protected]",
"candidate_name": "Jane Smith",
"tests": ["grammar-test", "editing-test"],
"expiry_hours": 120,
"reference": "JOB-4821",
"custom_message": "Thank you for applying. Please complete this assessment at your convenience."
}'
Example response 201 Created
{
"invitation": {
"id": "inv_k8x2m9",
"candidate_email": "[email protected]",
"candidate_name": "Jane Smith",
"tests": ["grammar-test", "editing-test"],
"status": "pending",
"credits_used": 1,
"reference": "JOB-4821",
"test_url": "https://editingtests.com/test/inv_k8x2m9/start",
"expires_at": "2026-04-22T14:00:00Z",
"created_at": "2026-04-17T14:00:00Z"
}
}
/api/v1/invitations/:id
Returns the current status of an invitation. Use this to poll for completion or check expiry.
Example request
curl https://editingtests.com/api/v1/invitations/inv_k8x2m9 \ -H "Authorization: Bearer et_live_xxxx"
Status values
"status": "pending" // Sent, not yet started "status": "in_progress" // Candidate has started "status": "completed" // All tests submitted — results available "status": "expired" // Link expired before completion "status": "cancelled" // Cancelled by client
/api/v1/results/:invitation_id
Returns the full result set for a completed invitation. Only available when status === "completed". Returns 404 for pending invitations.
Example request
curl https://editingtests.com/api/v1/results/inv_k8x2m9 \ -H "Authorization: Bearer et_live_xxxx"
Example response 200 OK
{
"result": {
"invitation_id": "inv_k8x2m9",
"candidate_name": "Jane Smith",
"candidate_email": "[email protected]",
"reference": "JOB-4821",
"completed_at": "2026-04-17T15:34:00Z",
"overall_pass": true,
"tests": [
{
"slug": "grammar-test",
"score": 84,
"percentile": 76,
"pass": true,
"duration_sec": 712,
"pass_threshold": 75
},
{
"slug": "editing-test",
"score": 91,
"percentile": 88,
"pass": true,
"duration_sec": 823,
"pass_threshold": 75
}
],
"integrity": {
"flagged": false,
"checks": {
"paste": { "flagged": false, "recorded": true, "detail": "No paste into an answer field." },
"cadence": { "flagged": false, "recorded": true, "detail": "1,412 keystrokes, 1,180 characters typed, 96 deleted, 100% of the text typed and 0% pasted; rhythm variation 0.81." },
"timing": { "flagged": false, "recorded": true, "detail": "20 questions timed; median 34 seconds on a question; 0 correct answers in under 5 seconds." },
"focus": { "flagged": false, "recorded": true, "detail": "1 tab switch and 0 fullscreen exits." }
},
"events": { "tab_switch": 1, "fullscreen_exit": 0, "copy_attempt": 0, "keyboard_blocked": 0, "paste": 0 }
},
"report_url": "https://editingtests.com/reports/inv_k8x2m9.pdf",
"report_expires_at": "2027-04-17T15:34:00Z"
}
}
Webhooks
Instead of polling for results, configure a webhook endpoint in your dashboard. EditingTests.com will POST to your URL when a candidate completes an assessment.
Events
| Event | Trigger |
|---|---|
| invitation.created | New invitation created via API or dashboard |
| invitation.opened | Candidate clicked the test link |
| assessment.started | Candidate began the first test |
| assessment.completed | All tests submitted and scored |
| assessment.expired | Invitation expired before completion |
Webhook payload — assessment.completed
{
"event": "assessment.completed",
"timestamp": "2026-04-17T15:34:00Z",
"data": {
"invitation_id": "inv_k8x2m9",
"candidate_email": "[email protected]",
"reference": "JOB-4821",
"overall_pass": true,
"result_url": "https://editingtests.com/api/v1/results/inv_k8x2m9"
}
}
Verifying webhook signatures
// Each webhook includes a signature header:
ET-Signature: sha256=abc123...
// Verify with your webhook secret:
const expected = crypto
.createHmac('sha256', process.env.ET_WEBHOOK_SECRET)
.update(rawBody)
.digest('hex');
if (expected !== req.headers['et-signature'].replace('sha256=','')) {
return res.status(401).send('Invalid signature');
}
SDKs & Examples
Quick-start examples in the most common server-side languages. All examples assume your API key is stored in an environment variable.
Node.js (fetch)
const response = await fetch('https://editingtests.com/api/v1/invitations', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.ET_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
candidate_email: '[email protected]',
candidate_name: 'Jane Smith',
tests: ['grammar-test', 'proofreading-test'],
reference: req.body.jobId,
}),
});
const { invitation } = await response.json();
console.log('Test URL:', invitation.test_url);
PHP (cURL)
$ch = curl_init('https://editingtests.com/api/v1/invitations');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('ET_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'candidate_email' => '[email protected]',
'candidate_name' => 'Jane Smith',
'tests' => ['grammar-test'],
'reference' => $jobId,
]),
]);
$result = json_decode(curl_exec($ch), true);
echo $result['invitation']['test_url'];
Python (requests)
import requests, os
response = requests.post(
'https://editingtests.com/api/v1/invitations',
headers={
'Authorization': f'Bearer {os.environ["ET_API_KEY"]}',
'Content-Type': 'application/json',
},
json={
'candidate_email': '[email protected]',
'candidate_name': 'Jane Smith',
'tests': ['grammar-test', 'writing-test'],
'reference': job_id,
}
)
invitation = response.json()['invitation']
print(invitation['test_url'])
Ready to Integrate?
API access is available on all accounts. Create a free account to get started, then generate your API key from the dashboard under Dashboard → API tokens.