Errors
All errors from the Buddy Punch Public API use a consistent JSON envelope, so you can handle them with a single parser. This guide covers the common (type, code) pairs you should expect, what to do with each, and when to retry. Individual endpoints may return additional codes for resource-specific situations (e.g., endpoint_limit_reached on webhooks) — always branch on type for retry logic, and use code + message for diagnostics.
Error Envelope
{
"error": {
"type": "invalid_request_error",
"code": "resource_not_found",
"message": "Employee 999 not found.",
"requestId": "req_abc123"
}
}
| Field | Type | Description |
|---|---|---|
type |
string | High-level category — one of the types listed below |
code |
string | Specific reason within the category |
message |
string | Human-readable explanation |
param |
string | (Optional) The specific parameter at fault — included on some validation failures, but not guaranteed on every 400 or parameter_invalid response |
errors |
array | (Optional) Per-field breakdown for some multi-field validation failures, each item { field, message }; not guaranteed to be present on every validation error |
requestId |
string | Opaque id. Include this when reporting issues to support |
Always branch on type for retry logic, not on HTTP status or message text. Messages are subject to change; types and codes are part of the contract.
HTTP Status → Error Type Mapping
| Status | Type | Typical Code | Retryable? |
|---|---|---|---|
400 |
invalid_request_error |
parameter_invalid |
No — fix the request |
401 |
authentication_error |
api_key_invalid |
No — fix credentials |
401 |
authentication_error |
legacy_key_unsupported |
No — switch to a bp_live_ key |
403 |
authorization_error |
insufficient_permissions |
No — grant missing scope |
404 |
invalid_request_error |
resource_not_found |
No — the id doesn't exist (or isn't yours) |
409 |
invalid_request_error |
idempotency_conflict |
No — fix the request or resolve the underlying conflict |
429 |
rate_limit_error |
rate_limit_exceeded |
Yes — honor Retry-After |
429 |
rate_limit_error |
account_rate_limit_exceeded |
Yes — honor Retry-After |
429 |
rate_limit_error |
trial_resource_limit_exceeded |
No — trial cap on this resource reached; upgrade or reduce volume |
429 |
rate_limit_error |
trial_resource_daily_limit_exceeded |
Wait for the rolling 24h window to free up, or upgrade |
429 |
rate_limit_error |
trial_daily_writes_exceeded |
Wait for the rolling 24h window to free up, or upgrade |
500 |
api_error |
internal_error |
Yes — exponential backoff, max 3 attempts |
Error Types in Detail
invalid_request_error
The request couldn't be processed as submitted. Your client should log the error and surface it to the caller — retries won't succeed.
| Code | Status | When |
|---|---|---|
parameter_invalid |
400 | A parameter failed validation (missing required field, wrong type, out of range). See the errors array for per-field detail. |
resource_not_found |
404 | The id in the path doesn't exist, OR exists in another account (we return 404 rather than 403 to avoid id-probing). |
idempotency_conflict |
409 | A conflict on a unique constraint — e.g., creating a resource that would duplicate one that already exists. Not emitted for idempotency-key replays (those always return the cached response; see the Idempotency guide). |
authentication_error
Your API key wasn't accepted.
| Code | Status | When |
|---|---|---|
api_key_invalid |
401 | Missing, malformed, expired, or revoked key |
legacy_key_unsupported |
401 | The request used a legacy APIM subscription key header. The new API accepts only Authorization: Bearer bp_live_…. See the Migration Guide. |
authorization_error
Authentication succeeded but the key isn't permitted to make this request.
| Code | Status | When |
|---|---|---|
insufficient_permissions |
403 | Your key doesn't hold the permission this endpoint requires (e.g., you called POST /employee with a key that only has employees:read). Grant the missing scope in the dashboard. |
Dashboard-only limit: Trial accounts may create only one active API key at a time. This is enforced in the Buddy Punch dashboard (you'll see an inline error when you try to create a second key) — it is not returned as an API response since there is no Public API endpoint for creating keys. Revoke the existing key or upgrade to a paid plan to add more.
rate_limit_error
You exceeded one of the platform's rate limits. Per-minute limits are scoped per account and depend on your plan: Trial 60 rpm, Pro/Premium/Starter 600 rpm, Enterprise 1,000 rpm. A separate per-key DoS guard at 1,000 rpm runs alongside the per-account limit. Trial accounts also have per-resource and daily write caps.
| Code | Status | When |
|---|---|---|
rate_limit_exceeded |
429 | Per-key DoS guard hit (1,000 rpm). Sleep Retry-After seconds and retry. |
account_rate_limit_exceeded |
429 | Your account-level per-minute quota was hit (60/600/1,000 rpm depending on plan). Sleep Retry-After seconds and retry; if you regularly hit it, distribute work or upgrade. |
trial_resource_limit_exceeded |
429 | Trial-only — you've reached the trial cap for this resource (e.g., total employees or webhooks created via the API). Reduce usage or upgrade to a paid plan. |
trial_resource_daily_limit_exceeded |
429 | Trial-only — you've reached the daily cap for this resource. Wait for the rolling 24h window to free up or upgrade. |
trial_daily_writes_exceeded |
429 | Trial-only — daily total of write requests across the account has been used up. Wait for the rolling 24h window to free up or upgrade. |
api_error
Something went wrong on our side.
| Code | Status | When |
|---|---|---|
internal_error |
500 | Unexpected server-side failure. Retry with exponential backoff. If it persists, report the requestId to support@buddypunch.com. |
Validation Errors
400 responses on POST/PUT requests may include per-field detail via the errors array:
{
"error": {
"type": "invalid_request_error",
"code": "parameter_invalid",
"message": "Validation failed for one or more fields.",
"errors": [
{ "field": "email", "message": "email must be a valid email address." },
{ "field": "punchInDatetime", "message": "punchInDatetime is required." }
],
"requestId": "req_abc123"
}
}
Use the errors array to highlight specific form fields in your UI. Fall back to message when the array is absent — some single-parameter failures only populate the top-level message.
Retry Strategy
| Error type | Retry? | Strategy |
|---|---|---|
rate_limit_error (rate_limit_exceeded, account_rate_limit_exceeded) |
Yes | Sleep Retry-After seconds, then retry. Back off exponentially on repeated 429s. |
rate_limit_error (trial_resource_limit_exceeded, trial_resource_daily_limit_exceeded, trial_daily_writes_exceeded) |
No (short-term) | Trial caps don't refresh within a Retry-After window — daily caps are measured over a rolling 24h window (old entries fall off gradually rather than resetting at a fixed time), and resource caps don't reset until usage drops. Surface the cap to the user or upgrade the plan rather than retrying in a tight loop. |
api_error (500) |
Yes | Exponential backoff starting at 1s, max 3 attempts (1s, 2s, 4s). |
authentication_error |
No | Don't retry. Fix the key. |
authorization_error |
No | Don't retry. Grant the missing scope, revoke a duplicate trial key, or upgrade. |
invalid_request_error |
No | The request is invalid or conflicts with current business state (including idempotency_conflict). Correct the request or resolve the conflict before trying again — a new idempotency key alone does not help. |
Upstream or proxy infrastructure may still return 502 Bad Gateway or 503 Service Unavailable — these are not Buddy Punch-emitted api_error responses but transient transport failures. Treat them the same way as api_error: retry with exponential backoff.
For write requests that are safe to retry but not idempotent (e.g., POST /punch), use the Idempotency-Key header so a network-timeout retry doesn't create a duplicate — see the Idempotency guide.
Example: Parsing Errors in Client Code
curl + jq
response=$(curl -s -w '\n%{http_code}' \
-H "Authorization: Bearer $BP_API_KEY" \
https://api2.buddypunch.com/employee/999)
body=$(echo "$response" | head -n 1)
status=$(echo "$response" | tail -n 1)
if [ "$status" -ge 400 ]; then
echo "$body" | jq -r '.error | "\(.type) / \(.code): \(.message) (reqId=\(.requestId))"'
fi
Node.js
const res = await fetch('https://api2.buddypunch.com/employee/999', {
headers: { 'Authorization': `Bearer ${process.env.BP_API_KEY}` }
});
if (!res.ok) {
const { error } = await res.json();
if (error.type === 'rate_limit_error') {
const retryAfter = Number(res.headers.get('retry-after') || 1);
await new Promise(r => setTimeout(r, retryAfter * 1000));
// retry…
} else if (error.type === 'api_error') {
// exponential backoff, max 3 attempts
} else {
console.error(`${error.type}/${error.code}: ${error.message} (req=${error.requestId})`);
}
}
Related Guides
- Getting Started — first request, envelope basics
- Data Model — resource field reference
- Idempotency — safe retries for writes
- Code Examples — complete error-handling samples per language