Webhook’lar
Solidus entegrasyonunuzda olaylar gerçekleştiğinde gerçek zamanlı bildirimler alın. Webhook’lar, bir doğrulama tamamlandığında, bir kimlik bilgisi düzenlendiğinde veya başka olaylar meydana geldiğinde uç noktanıza HTTP POST istekleri teslim eder.
Temel URL
https://verify.solidus.network/v1API anahtarınızla istekleri kimlik doğrulayın:
Authorization: Bearer YOUR_API_KEY1. Bir Webhook Uç Noktası Oluşturun
Webhook olaylarını almak için bir URL kaydedin.
const response = await fetch('https://verify.solidus.network/v1/webhooks', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_API_KEY',
},
body: JSON.stringify({
url: 'https://myapp.com/webhooks/solidus',
events: [
'verification.completed',
'verification.failed',
'verification.expired',
'credential.issued',
'credential.revoked',
],
description: 'Production webhook for KYC events',
}),
})
const webhook = await response.json()Yanıt:
{
"id": "wh_3nK8mR2pQ5",
"url": "https://myapp.com/webhooks/solidus",
"events": [
"verification.completed",
"verification.failed",
"verification.expired",
"credential.issued",
"credential.revoked"
],
"description": "Production webhook for KYC events",
"secret": "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"status": "active",
"createdAt": "2026-05-07T12:00:00Z"
}secret değerini kaydedin. Webhook imzalarını doğrulamak için onu kullanacaksınız. Sır, yalnızca
oluşturma sırasında bir kez döndürülür.
2. Desteklenen Olaylar
| Event | Trigger |
|---|---|
verification.completed | KYC doğrulaması geçti, kimlik bilgisi düzenlendi |
verification.failed | KYC doğrulaması başarısız oldu (belge sorunları, yüz uyuşmazlığı) |
verification.expired | Doğrulama oturumu tamamlanmadan önce sona erdi |
credential.issued | Yeni bir Doğrulanabilir Kimlik Bilgisi düzenlendi |
credential.revoked | Mevcut bir kimlik bilgisi iptal edildi |
3. Webhook Yük (Payload) Biçimi
Her webhook teslimatı, aşağıdaki yapıya sahip bir JSON yükü gönderir:
{
"id": "evt_8nR3kL5mQ2xY",
"type": "verification.completed",
"createdAt": "2026-05-07T12:05:30Z",
"data": {
"verificationId": "ver_2xK9mP4qR7nL",
"status": "completed",
"outcome": "pass",
"subjectDid": "did:solidus:testnet:7Hk3mRtQZv...",
"level": 1,
"credentialId": "urn:uuid:a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}Olay Türüne Göre Yük
verification.completed
{
"id": "evt_8nR3kL5mQ2xY",
"type": "verification.completed",
"createdAt": "2026-05-07T12:05:30Z",
"data": {
"verificationId": "ver_2xK9mP4qR7nL",
"status": "completed",
"outcome": "pass",
"subjectDid": "did:solidus:testnet:7Hk3mRtQZv...",
"level": 1,
"credentialId": "urn:uuid:a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}verification.failed
{
"id": "evt_9pQ4rM6sT3wZ",
"type": "verification.failed",
"createdAt": "2026-05-07T12:05:30Z",
"data": {
"verificationId": "ver_2xK9mP4qR7nL",
"status": "failed",
"outcome": "fail",
"subjectDid": "did:solidus:testnet:7Hk3mRtQZv...",
"reason": "face_mismatch",
"message": "The selfie does not match the photo on the identity document."
}
}credential.issued
{
"id": "evt_5kN2jH8bF7mR",
"type": "credential.issued",
"createdAt": "2026-05-07T12:05:30Z",
"data": {
"credentialId": "urn:uuid:a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"type": ["VerifiableCredential", "KYCCredential"],
"issuer": "did:solidus:testnet:<verify-service>",
"subjectDid": "did:solidus:testnet:7Hk3mRtQZv...",
"issuanceDate": "2026-05-07T12:05:30Z",
"expirationDate": "2027-05-07T12:05:30Z",
"txHash": "0x7a3b9c2d1e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b"
}
}credential.revoked
{
"id": "evt_4mL9gK3cE6nP",
"type": "credential.revoked",
"createdAt": "2026-05-07T14:20:00Z",
"data": {
"credentialId": "urn:uuid:a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"subjectDid": "did:solidus:testnet:7Hk3mRtQZv...",
"revokedBy": "did:solidus:testnet:<verify-service>",
"reason": "Information no longer accurate",
"txHash": "0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b"
}
}4. İmza Doğrulama
Her webhook isteği, Solidus-Signature başlığında bir imza içerir. İsteğin Solidus’tan geldiğini
doğrulamak için her zaman bu imzayı kontrol edin.
Başlık, virgülle ayrılmış iki alan taşır:
Solidus-Signature: t=1758579012,v1=a1b2c3d4e5f6...t, saniye cinsinden Unix zaman damgasıdır. v1 ise HMAC-SHA256 hex özetidir. Ayrı bir zaman
damgası başlığı yoktur; t alanını bu başlıktan ayrıştırın.
İmzalanan metin, zaman damgası, bir satır sonu ve ham istek gövdesidir:
t=<timestamp>\n<raw body>HMAC anahtarı webhook sırrınızdır. Yalnızca gövdeyi değil, birleştirilmiş metni imzalayın.
import crypto from 'crypto'
function parseSignatureHeader(
header: string
): { timestamp: number; signature: string } | null {
const parts = new Map<string, string>()
for (const field of header.split(',')) {
const [key, value] = field.split('=', 2)
if (key && value) parts.set(key.trim(), value.trim())
}
const t = parts.get('t')
const v1 = parts.get('v1')
if (!t || !v1) return null
const timestamp = Number.parseInt(t, 10)
if (!Number.isFinite(timestamp)) return null
return { timestamp, signature: v1 }
}
function verifyWebhookSignature(
rawBody: string,
header: string,
secret: string
): boolean {
const parsed = parseSignatureHeader(header)
if (!parsed) return false
const expected = crypto
.createHmac('sha256', secret)
.update(`t=${parsed.timestamp}\n${rawBody}`)
.digest('hex')
// timingSafeEqual throws when the two buffers differ in length. Compare the
// lengths first, then compare the bytes.
const given = Buffer.from(parsed.signature, 'utf8')
const want = Buffer.from(expected, 'utf8')
if (given.length !== want.length) return false
return crypto.timingSafeEqual(given, want)
}Zaman damgası aynı başlığın içinde, t alanı olarak gelir. Ayrı bir zaman damgası başlığı yoktur.
Yeniden oynatma saldırılarını önlemek için t alanı 5 dakikadan daha eski olan istekleri reddedin.
5. Tam Express Webhook İşleyicisi
Solidus webhook’larını alan, doğrulayan ve işleyen üretime hazır bir Express işleyicisi.
import express from 'express'
import crypto from 'crypto'
const app = express()
const WEBHOOK_SECRET = process.env.SOLIDUS_WEBHOOK_SECRET!
// Use raw body for signature verification
app.post(
'/webhooks/solidus',
express.raw({ type: 'application/json' }),
async (req, res) => {
const header = req.headers['solidus-signature'] as string
const rawBody = req.body.toString()
// 1. Parse the header. It carries the timestamp and the digest together.
const parsed = header ? parseSignatureHeader(header) : null
if (!parsed) {
res.status(400).json({ error: 'Missing or malformed signature header' })
return
}
// 2. Verify the timestamp is recent (within 5 minutes)
const eventTime = parsed.timestamp * 1000
const now = Date.now()
if (Math.abs(now - eventTime) > 5 * 60 * 1000) {
res.status(400).json({ error: 'Timestamp too old' })
return
}
// 3. Verify the signature over `t=<timestamp>\n<raw body>`
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(`t=${parsed.timestamp}\n${rawBody}`)
.digest('hex')
const given = Buffer.from(parsed.signature, 'utf8')
const want = Buffer.from(expected, 'utf8')
const isValid =
given.length === want.length && crypto.timingSafeEqual(given, want)
if (!isValid) {
res.status(401).json({ error: 'Invalid signature' })
return
}
// 3. Parse the event
const event = JSON.parse(rawBody)
// 4. Respond immediately with 200 to acknowledge receipt
res.status(200).json({ received: true })
// 5. Process the event asynchronously
try {
switch (event.type) {
case 'verification.completed':
await handleVerificationCompleted(event.data)
break
case 'verification.failed':
await handleVerificationFailed(event.data)
break
case 'verification.expired':
await handleVerificationExpired(event.data)
break
case 'credential.issued':
await handleCredentialIssued(event.data)
break
case 'credential.revoked':
await handleCredentialRevoked(event.data)
break
default:
console.log('Unknown event type:', event.type)
}
} catch (err) {
console.error('Error processing webhook:', err)
}
}
)
async function handleVerificationCompleted(data: any) {
// Update user record in your database
await db.users.update({
where: { did: data.subjectDid },
data: {
kycStatus: 'verified',
kycLevel: data.level,
credentialId: data.credentialId,
verifiedAt: new Date(),
},
})
}
async function handleVerificationFailed(data: any) {
await db.users.update({
where: { did: data.subjectDid },
data: {
kycStatus: 'failed',
kycFailureReason: data.reason,
},
})
}
async function handleVerificationExpired(data: any) {
await db.users.update({
where: { did: data.subjectDid },
data: { kycStatus: 'expired' },
})
}
async function handleCredentialIssued(data: any) {
await db.credentials.create({
data: {
credentialId: data.credentialId,
subjectDid: data.subjectDid,
type: data.type,
issuer: data.issuer,
issuedAt: new Date(data.issuanceDate),
expiresAt: new Date(data.expirationDate),
txHash: data.txHash,
},
})
}
async function handleCredentialRevoked(data: any) {
await db.credentials.update({
where: { credentialId: data.credentialId },
data: {
revokedAt: new Date(),
revokedBy: data.revokedBy,
revocationReason: data.reason,
},
})
}
app.listen(3001)6. Teslimatı İşleme
Webhook teslimatlarını güvenilir şekilde işlemek için şu uygulamaları izleyin:
Hızlı yanıt verin. Mümkün olduğunca hızlı bir 200 durum kodu döndürün. Yanıt verdikten sonra
olayı eşzamansız olarak işleyin. Uç noktanız çok uzun sürerse (10 saniyeden fazla), teslimat
başarısız olarak işaretlenir.
Yinelenenleri işleyin. Webhook teslimatları yeniden denenebilir, bu yüzden aynı olayı birden
fazla kez alabilirsiniz. Olayları tekilleştirmek için id alanını kullanın.
// Deduplicate using the event ID
const alreadyProcessed = await db.processedEvents.findUnique({
where: { eventId: event.id },
})
if (alreadyProcessed) {
res.status(200).json({ received: true, duplicate: true })
return
}
// Mark as processed before handling
await db.processedEvents.create({
data: { eventId: event.id, processedAt: new Date() },
})HTTPS kullanın. Webhook uç noktaları üretimde HTTPS kullanmalıdır. HTTP uç noktalarına yalnızca
geliştirme sırasında localhost için izin verilir.
7. Yeniden Deneme Politikası
Uç noktanız 2xx olmayan bir durum kodu döndürürse ya da 10 saniye içinde yanıt vermezse, Solidus teslimatı üstel geri çekilme (exponential backoff) ile yeniden dener. Toplam 6 deneme yapılır ve geri çekilme 10 saniyeden başlar:
| Deneme | İlk denemeden sonra ne zaman |
|---|---|
| 1. deneme | 0s |
| 2. deneme | ~10s |
| 3. deneme | ~20s |
| 4. deneme | ~40s |
| 5. deneme | ~80s |
| 6. deneme | ~160s |
Tüm yeniden deneme penceresi yaklaşık 5 dakikadır. 6. denemeden sonra teslimat kalıcı olarak başarısız işaretlenir. Buna göre plan yapın: 10 dakika boyunca kapalı kalan bir uç nokta olayı kaybeder. Başarısız teslimatları kontrol panelinde veya API aracılığıyla görebilir, oradan yeniden gönderebilirsiniz.
2026-09-23’te düzeltildi. Bu sayfanın önceki hâli, yaklaşık 14 saate yayılan 5 yeniden deneme ve 30 saniyelik bir zaman aşımı anlatıyordu. İkisi de kodla uyuşmuyordu. Yukarıdaki tablo,
verify/apps/backend/src/modules/webhooks/service.tsdosyasından okunan gerçek davranıştır.
8. Teslimat Günlükleri
Bir webhook uç noktası için teslimat geçmişini inceleyin.
const response = await fetch(
'https://verify.solidus.network/v1/webhooks/wh_3nK8mR2pQ5/deliveries',
{
headers: { 'Authorization': 'Bearer YOUR_API_KEY' },
}
)
const deliveries = await response.json()Yanıt:
{
"data": [
{
"id": "del_7rT2nK9mP4",
"eventId": "evt_8nR3kL5mQ2xY",
"eventType": "verification.completed",
"status": "delivered",
"statusCode": 200,
"attemptNumber": 1,
"requestTimestamp": "2026-05-07T12:05:31Z",
"responseTimestamp": "2026-05-07T12:05:31Z",
"duration": 145
},
{
"id": "del_4mL9gK3cE6",
"eventId": "evt_9pQ4rM6sT3wZ",
"eventType": "verification.failed",
"status": "failed",
"statusCode": 500,
"attemptNumber": 1,
"nextRetryAt": "2026-05-07T12:11:30Z",
"requestTimestamp": "2026-05-07T12:05:30Z",
"responseTimestamp": "2026-05-07T12:05:32Z",
"duration": 2045
}
],
"pagination": {
"total": 2,
"page": 1,
"perPage": 25
}
}9. Webhook’ları Test Etme
Doğru yapılandırıldığını doğrulamak için uç noktanıza bir test olayı gönderin.
const response = await fetch(
'https://verify.solidus.network/v1/webhooks/test',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_API_KEY',
},
body: JSON.stringify({
webhookId: 'wh_3nK8mR2pQ5',
eventType: 'verification.completed',
}),
}
)
const result = await response.json()Yanıt:
{
"id": "del_test_9kM2nR5pQ3",
"status": "delivered",
"statusCode": 200,
"duration": 230,
"request": {
"url": "https://myapp.com/webhooks/solidus",
"headers": {
"Content-Type": "application/json",
"Solidus-Signature": "t=1758579012,v1=a1b2c3d4..."
}
}
}Test olaylarının, gerçek olaylardan ayırt edebilmeniz için evt_test_ önekli bir id’si vardır.
10. Webhook’ları Yönetme
Webhook’ları Listele
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://verify.solidus.network/v1/webhooksBir Webhook’u Güncelle
curl -X PATCH https://verify.solidus.network/v1/webhooks/wh_3nK8mR2pQ5 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"events": ["verification.completed", "credential.issued"],
"status": "active"
}'Bir Webhook’u Sil
curl -X DELETE https://verify.solidus.network/v1/webhooks/wh_3nK8mR2pQ5 \
-H "Authorization: Bearer YOUR_API_KEY"Sırrı Döndür (Rotate)
Webhook sırrınız ele geçirilirse, onu döndürün. Eski sır anında geçersiz hale gelir.
curl -X POST \
https://verify.solidus.network/v1/webhooks/wh_3nK8mR2pQ5/rotate-secret \
-H "Authorization: Bearer YOUR_API_KEY"Güvenlik Kontrol Listesi
- Olayları işlemeden önce her zaman
Solidus-Signaturebaşlığını doğrulayın - Zaman damgaları 5 dakikadan daha eski olan istekleri reddedin
- Üretimde HTTPS uç noktaları kullanın
- Webhook sırrını güvenli bir şekilde saklayın (ortam değişkeni, sır yöneticisi)
- Olay
idalanını kullanarak olayları tekilleştirin 200döndürdükten sonra olayları eşzamansız olarak işleyin- Kalıcı başarısızlıklar için teslimat günlüklerini izleyin
Sonraki Adımlar
- KYC Entegrasyonu - doğrulama akışını gömün
- Kimlik Bilgisi Akışı - tam kimlik bilgisi yaşam döngüsü
- API Referansı - tam API belgeleri