Error handling
Every Silky API response - success or failure - carries a consistent envelope.
Success
{
"success": true,
"data": { ... },
"meta": { "request_id": "req_abc123def456" },
"error": null
}
Failure
{
"success": false,
"data": null,
"meta": { "request_id": "req_abc123def456" },
"error": {
"type": "validation",
"code": "VALIDATION_ERROR",
"message": "email is required.",
"field": "email",
"docs": "https://app.silky.so/docs/agents/error-handling#validation"
}
}
Always log request_id
Every response includes meta.request_id. Log it on every API call you make. If you file a support ticket or need the team to trace an issue, the request ID is the fastest path from your log line to our traces in Sentry + Inngest.
HTTP header form: X-Request-ID. Both are set on every response.
Error types
error.type | HTTP | When it happens | What to do |
|---|---|---|---|
validation | 400 | Missing or malformed field | Fix and retry. error.field points at the offender. |
unauthenticated | 401 | Missing or invalid API key | Check Authorization: Bearer sk_... header. |
permission_denied | 403 | API key scope does not cover this action | Use an admin-scoped key or narrow the request. |
not_found | 404 | Resource does not exist (or RLS hid it) | The ID is wrong or belongs to another account. |
conflict | 409 | Duplicate resource or state mismatch | Handle as a no-op or pick a different identifier. |
rate_limited | 429 | Too many requests | Read Retry-After, back off, retry. |
internal | 500 | Our fault | Retry with backoff. If persistent, file with the request_id. |
service_unavailable | 503 | Upstream down (AI provider, etc.) | Retry after 30s. |
Idempotency
Every mutating endpoint accepts an Idempotency-Key header. Use a UUID per logical operation. Retries with the same key return the original response - no duplicate resources.
curl -X POST $SILKY_HOST/api/v1/applications/app_01HXXX/transition \
-H "Authorization: Bearer $SILKY_API_KEY" \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-H "Content-Type: application/json" \
-d '{ "to_stage": "shortlisted" }'
Keys are scoped per API key and valid for 24 hours. After that the key is forgotten and a retry creates a fresh operation.
Retry rules
Safe to retry: any 5xx, any 429, any network error.
Safe to retry only with an idempotency key on mutating calls: POST, PATCH, DELETE. Without the key, a retry may create a duplicate.
Never retry: 4xx (except 429). They will not succeed no matter how many times you try.
Exponential backoff starting at 1 second, doubling to a max of 60. Stop after 5 attempts.
Rate limits
Every response includes:
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 598
X-RateLimit-Reset: 1730000000
Free tier: 100 requests / minute per API key. Paid tiers lift this to 600+ depending on plan. Burst allowance sits on top: you can spike to 2x the limit for up to 10 seconds before getting throttled.
When you hit 429, Retry-After is the absolute minimum wait in seconds. Respect it. Aggressive clients get IP-blocked.
Bulk operations
If you find yourself making >50 calls in a row for the same type of operation, switch to POST /api/v1/batch. One call, up to 100 operations, all counted as a single rate-limit unit.
curl -X POST $SILKY_HOST/api/v1/batch \
-H "Authorization: Bearer $SILKY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"operations": [
{ "method": "POST", "path": "/applications/app_1/transition", "body": { "to_stage": "rejected" } },
{ "method": "POST", "path": "/applications/app_2/transition", "body": { "to_stage": "shortlisted" } }
]
}'
Batch is all-or-nothing by default. Set atomic: false if you want partial success semantics.
Debugging checklist
When a call fails and the error message is not obvious:
- Check
error.docs- points to the exact docs section for the error. - Copy the
request_id. Paste into any support message. - Verify the key is live:
GET /api/v1/accounts/me. If that returns 200, your auth is fine; the issue is in the specific endpoint. - Verify the resource exists and belongs to the authed account:
GET /api/v1/{resource}/{id}. A 404 here means wrong account or deleted resource. - For async tasks, check the task state:
GET /api/v1/tasks/{task_id}. Error details live there, not on the originating call.