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.typeHTTPWhen it happensWhat to do
validation400Missing or malformed fieldFix and retry. error.field points at the offender.
unauthenticated401Missing or invalid API keyCheck Authorization: Bearer sk_... header.
permission_denied403API key scope does not cover this actionUse an admin-scoped key or narrow the request.
not_found404Resource does not exist (or RLS hid it)The ID is wrong or belongs to another account.
conflict409Duplicate resource or state mismatchHandle as a no-op or pick a different identifier.
rate_limited429Too many requestsRead Retry-After, back off, retry.
internal500Our faultRetry with backoff. If persistent, file with the request_id.
service_unavailable503Upstream 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:

  1. Check error.docs - points to the exact docs section for the error.
  2. Copy the request_id. Paste into any support message.
  3. 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.
  4. Verify the resource exists and belongs to the authed account: GET /api/v1/{resource}/{id}. A 404 here means wrong account or deleted resource.
  5. For async tasks, check the task state: GET /api/v1/tasks/{task_id}. Error details live there, not on the originating call.