API Documentation
API Documentation that will walk you through how to create your API key, common use cases, and security best practices.
Overview
The Herd API provides programmatic access to training completion data, user information, and tag management. All endpoints require API key authentication and are scoped to your organization.
Creating your API Key
Getting Your API Key
Log into your Herd dashboard
Navigate to Administration on the left sidebar > API Keys
Click "Create New API Key"
Save the key securely - Please note: The key will only be shown once

Using Your API Key
Include your API key in the 'X-API-Key' header with every request:
curl -H "X-API-Key: herd_a1b2c3d4_..." https://app.security.io/api/v1/enrollmentsAPI Key Format: herd_<8chars>_<64chars> (73 characters total)
Response Format
All API responses follow this structure:
{
"status": "success",
"data": [...],
"pagination": { "
total_records": 100,
"current_page": 1,
"total_pages": 2,
"per_page": 50,
"next_page_url": "https://app.herdsecurity.io/api/v1/enrollments?page=2&per_page=50" }
}{
"status": "error",
"error": {
"code": 401,
"message": "Invalid API key",
"details": {}
}
} Endpoints
Get Training Completions
GET https://api.herdsecurity.io/api/v1/enrollmentsRetrieves all training enrollments and completion status for your organization. This is the primary endpoint for tracking training progress and completions.
Query Parameters:
page
integer
No
1
Page number (min: 1)
per_page
integer
No
50
Results per page (min: 1, max: 100)
{
"status": "success",
"data": [
{
"enrollment_id": "enr_clxyz456ghi",
"user_id": "usr_clxyz789def",
"training_id": "trn_clxyz123abc",
"tag_id": "tag_UGhpc2hpbmc",
"enrollment_date": "2024-01-15T10:30:00.000Z",
"start_date": "2024-01-15T14:20:00.000Z",
"completion_date": "2024-01-16T09:45:00.000Z",
"status": "Passed"
}
],
"pagination": {
"total_records": 500,
"current_page": 1,
"total_pages": 10,
"per_page": 50
}
}Status Values:
Not started- User has not begun the trainingIn Progress- User started but has not completedPassed- User completed the training
Date Fields:
enrollment_date- When user was enrolled (always present)start_date- When user started training (null if not started)completion_date- When user completed training (null if not completed)
Example Request:
curl -H "X-API-Key: herd_a1b2c3d4_..." \ "https://app.security.io/api/v1/enrollments?page=1&per_page=100`
Use Cases:
Export completed trainings to Google Sheets
Generate completion reports
Track training progress across your organization
Identify users who need to complete specific trainings
2. Get Users
GET /api/v1/users
Retrieves user details for your organization.
Query Parameters:
page
integer
No
1
Page number
per_page
integer
No
50
Results per page (max: 100)
{
"status": "success",
"data": [
{
"user_id": "usr_clxyz789def",
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@company.com",
"status": "Active"
}
],
"pagination": {
"total_records": 250,
"current_page": 1,
"total_pages": 5,
"per_page": 50
}
}
```Status Values:
Active- User has Slack or Teams integrationInactive- User not connected to any platform
Get Tags
GET /api/v1/tags
Retrieves all tags used to categorize trainings in your organization.
Query Parameters:
page
integer
No
1
Page number
per_page
integer
No
50
Results per page (max: 100)
{
"status": "success",
"data": [
{
"tag_id": "tag_UGhpc2hpbmc",
"name": "Phishing Awareness",
"status": "In Progress",
"start_date": null,
"end_date": null,
"created_at": "2024-01-15T10:30:00.000Z"
}
],
"pagination": {
"total_records": 25,
"current_page": 1,
"total_pages": 1,
"per_page": 50
}
}
```{
"status": "success",
"tag_id": "tag_UGhpc2hpbmc",
"data": [
{
"training_id": "trn_clxyz123abc",
"title": "Phishing Detection 101",
"description": "Learn to identify phishing attempts"
}
],
"pagination": {
"total_records": 10,
"current_page": 1,
"total_pages": 1,
"per_page": 50
}
}Common Use Cases
Filter by Date Range
const enrollments = await fetch('https://your-domain.com/api/v1/enrollments?per_page=100', {
headers: { 'X-API-Key': 'herd_...' }
}).then(r => r.json());
// Filter completions from last 30 days
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const recentCompletions = enrollments.data.filter(e => {
if (!e.completion_date) return false;
return new Date(e.completion_date) >= thirtyDaysAgo;
});Group Completions by User
const enrollments = await fetch('https://your-domain.com/api/v1/enrollments?per_page=100', {
headers: { 'X-API-Key': 'herd_...' }
}).then(r => r.json());
const byUser = enrollments.data.reduce((acc, enrollment) => {
if (!acc[enrollment.user_id]) {
acc[enrollment.user_id] = {
total: 0,
completed: 0,
in_progress: 0,
not_started: 0
};
}
acc[enrollment.user_id].total++;
if (enrollment.status === 'Passed') acc[enrollment.user_id].completed++;
else if (enrollment.status === 'In Progress') acc[enrollment.user_id].in_progress++;
else acc[enrollment.user_id].not_started++;
return acc;
}, {});
console.log(byUser);Pagination
All list endpoints support pagination. When results exceed one page, use the next_page_url or increment the page parameter.
Example: Fetch all enrollments
async function fetchAllEnrollments(apiKey) {
let allEnrollments = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(
`https://your-domain.com/api/v1/enrollments?page=${page}&per_page=100`,
{ headers: { 'X-API-Key': apiKey } }
);
const result = await response.json();
allEnrollments = allEnrollments.concat(result.data);
hasMore = page < result.pagination.total_pages;
page++;
}
return allEnrollments;
}Error Codes
400
Bad Request
Check query parameters are valid
401
Unauthorized
Verify API key is correct and active
404
Not Found
Resource doesn't exist (e.g., invalid tag_id)
429
Too Many Requests
Slow down request rate
500
Internal Server Error
Contact support if persists
Rate Limiting
Rate limits are enforced to ensure API stability. If you receive a 429 error, implement exponential backoff:
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
const response = await fetch(url, options);
if (response.status !== 429) {
return response;
}
// Exponential backoff: 1s, 2s, 4s
const delay = Math.pow(2, i) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
throw new Error('Max retries exceeded');
}Last updated