Webhooks
Silky fires webhooks on every meaningful state change. Prefer webhooks to polling whenever possible - they are cheaper for both sides and latency is sub-second.
Subscribe
curl -X POST $SILKY_HOST/api/v1/webhooks \
-H "Authorization: Bearer $SILKY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.example.com/silky-webhook",
"events": ["application.created", "application.assessed", "offer.accepted"]
}'
Response:
{
"success": true,
"data": {
"id": "whk_01HXXX...",
"url": "https://your-app.example.com/silky-webhook",
"events": ["application.created", "application.assessed", "offer.accepted"],
"secret": "whsec_abc123...",
"status": "active"
}
}
The secret is returned once. Store it. It is the HMAC signing key for verifying deliveries.
Verify the signature
Every delivery includes two headers:
X-Silky-Timestamp: 1730000000
X-Silky-Signature: t=1730000000,v1=<hex-hmac-sha256>
Signature formula: HMAC-SHA256(secret, timestamp + "." + raw_body).
Node.js verification
import crypto from 'node:crypto';
function verify(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(
header.split(',').map(pair => pair.split('='))
);
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))) {
throw new Error('Bad signature');
}
// Reject replay attacks - timestamp older than 5 minutes.
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) {
throw new Error('Stale webhook');
}
}
Python verification
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str) -> None:
parts = dict(pair.split('=') for pair in header.split(','))
t, v1 = parts['t'], parts['v1']
expected = hmac.new(
secret.encode(), f"{t}.{raw_body.decode()}".encode(), hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, v1):
raise ValueError('Bad signature')
if abs(time.time() - int(t)) > 300:
raise ValueError('Stale webhook')
Event catalogue
| Event | Fires when | Payload highlights |
|---|---|---|
account.created | Signup completes | account_id, owner_email |
account.enriched | Async enrichment finishes | brand_colors, logo_url, timezone |
job.created | Job spec drafted + ready | job_id, spec, assessment_criteria |
job.published | Job ad goes live | job_id, share_links |
application.created | Candidate submits | application_id, candidate_id, job_id |
application.parsed | Resume parse finishes | resume_data |
application.assessed | AI assessment completes | score_total, per_criterion_scores |
application.stage_changed | Any transition | from_stage, to_stage, reason |
interview.scheduled | New interview | interview_id, starts_at, meet_url |
interview.completed | Scorecard submitted | scorecard, recommendation |
reference.submitted | Referee responds | reference_id, red_flags |
offer.sent | Offer emailed to candidate | offer_id, amount, currency |
offer.accepted | Candidate signs | offer_id, signed_at |
offer.declined | Candidate declines | offer_id, reason |
hire.created | Application moves to hired | hire_id, start_date |
Full payload schemas in the OpenAPI spec at /api/v1/openapi.json under components.schemas.WebhookEvent*.
Delivery guarantees
- At-least-once delivery. Deduplicate on
event.id. - Exponential backoff on failure: 30s, 2m, 10m, 1h, 6h, 24h. Six attempts total. After that we give up and mark the subscription as unhealthy.
- A 2xx response within 10 seconds counts as success. Anything else is a failure.
- Subscriptions that fail 10 deliveries in a row are auto-disabled. You will get an
account.webhook_disabledevent to your remaining active subs.
Rotating a secret
curl -X POST $SILKY_HOST/api/v1/webhooks/whk_01HXXX/rotate-secret \
-H "Authorization: Bearer $SILKY_API_KEY"
Returns a new whsec_.... The old secret keeps working for 24 hours to give you a window to update your verifier.
Deleting a subscription
curl -X DELETE $SILKY_HOST/api/v1/webhooks/whk_01HXXX \
-H "Authorization: Bearer $SILKY_API_KEY"
Hand this to Claude
Subscribe a webhook at https://my-worker.example.com/silky to the events
application.assessed and offer.accepted. Store the returned secret in
our secrets manager under SILKY_WEBHOOK_SECRET. Show me the subscription
ID when done.