Skip to Content
GuidesWebhooks

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/v1

API anahtarınızla istekleri kimlik doğrulayın:

Authorization: Bearer YOUR_API_KEY

1. 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

EventTrigger
verification.completedKYC doğrulaması geçti, kimlik bilgisi düzenlendi
verification.failedKYC doğrulaması başarısız oldu (belge sorunları, yüz uyuşmazlığı)
verification.expiredDoğrulama oturumu tamamlanmadan önce sona erdi
credential.issuedYeni bir Doğrulanabilir Kimlik Bilgisi düzenlendi
credential.revokedMevcut 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. deneme0s
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.ts dosyası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/webhooks

Bir 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-Signature baş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 id alanını kullanarak olayları tekilleştirin
  • 200 dö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

Last updated on