Skip to main content

REST API

API Documentation

Integrate EditingTests.com into your ATS, HRIS, or HR platform. Invite candidates, retrieve results, and automate your editorial hiring workflow.

Base URL https://editingtests.com/api/v1
Version v1 (stable)
Format 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.

API rate limits by endpoint
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"
  }
}
API error codes and their meanings
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
GET /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
  ]
}
POST /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

Request body parameters
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"
  }
}
GET /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
GET /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

Webhook events and what triggers them
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.

Editors from these organizations have used our services since 1998

Reuters BBC Oxford University Press Penguin Random House Springer Microsoft Suncor Energy United Nations Fisher Investments IBM The Home Depot KODAK CHEVRON