Eyena Embed API — tam rehber
Bu bölüm kimler için?
Kendi sorularınızı ve ödevlerinizi Eyena'ya programatik olarak eklemek isteyen 3. taraf entegratörler. Eyena Sistem Yetkilisi bu işlemleri portal üzerinden de yapabilir; API yolu otomasyon/toplu içerik yükleme için kullanışlıdır.
Eyena Sistem Yetkilisi'nden client_id (embed-… formatı) ve client_secret alın. Bu kimlikler hem içerik yönetimi hem de iframe oturumları için kullanılır.
Client credentials akışıyla eyena-giris'ten bir access token alın. Bu istek kesinlikle sunucu tarafında yapılmalıdır.
// Sunucu tarafı
const res = await fetch('https://giris.eyena.net/connect/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: 'embed-abc123', // Sistem Yetkilisi'nden
client_secret: 'GIZLI_SECRET', // .env'de tutun
scope: 'api',
}),
})
const { access_token } = await res.json()Soruların içeriği tek bir görseldir; istek multipart/form-dataolarak gönderilmelidir. Sorunun sahibi otomatik olarak token'daki client_id olur — başka bir istemcinin sorusunu oluşturmanız veya görmeniz mümkün değildir.
// POST https://api.eyena.net/api/embed/sorular (multipart/form-data)
const form = new FormData()
form.append('Baslik', '1. Soru')
form.append('Gorsel', gorselDosyasi) // File — JPEG/PNG/GIF/WebP, max 5 MB
form.append('SeceneklerJson', JSON.stringify([
{ metin: 'Birinci şık', dogruMu: true, sira: 1 },
{ metin: 'İkinci şık', dogruMu: false, sira: 2 },
{ metin: 'Üçüncü şık', dogruMu: false, sira: 3 },
]))
form.append('BolgelerJson', JSON.stringify([ // opsiyonel — heatmap analizi için ilgi alanları
{ x: 10, y: 20, genislik: 200, yukseklik: 150, aciklama: 'Grafik', sira: 1 },
]))
const res = await fetch('https://api.eyena.net/api/embed/sorular', {
method: 'POST',
headers: { 'Authorization': `Bearer ${access_token}` },
// Content-Type başlığını EKLEMEYİN — tarayıcı/fetch boundary'yi otomatik ayarlar
body: form,
})
const soru = await res.json()
// soru.id ← ödevinize eklemek için bu ID'yi saklayınGörsel Azure Blob Storage'a yüklenir; GET /api/embed/sorular/{id} yanıtındaki icerikalanı zaten çözümlenmiş bir URL içerir. JPEG, PNG, GIF ve WebP desteklenir; maksimum boyut 5 MB'dır.
// Kendi sorularınızı listele
GET https://api.eyena.net/api/embed/sorular
// Tek soru detayı (icerik = çözümlenmiş görsel URL'si)
GET https://api.eyena.net/api/embed/sorular/{id}
// Güncelle (multipart/form-data)
// Gorsel alanı opsiyoneldir — gönderilmezse mevcut görsel korunur;
// gönderilirse eski görsel silinir, yenisi yüklenir.
const form = new FormData()
form.append('Baslik', 'Güncel başlık')
form.append('Gorsel', yeniGorselDosyasi) // opsiyonel
form.append('SeceneklerJson', JSON.stringify([...]))
form.append('BolgelerJson', JSON.stringify([...]))
await fetch(`https://api.eyena.net/api/embed/sorular/${soru.id}`, {
method: 'PUT',
headers: { 'Authorization': `Bearer ${access_token}` },
body: form,
})
// Sil — görsel de otomatik olarak silinir
// (ödev bağlantısı varsa HTTP 409 döner — önce ödevden çıkarın)
DELETE https://api.eyena.net/api/embed/sorular/{id}Tüm istekler Authorization: Bearer {access_token} başlığı gerektirir ve yalnızca kendi istemcinize ait sorularla çalışır.
Ödevlerin slug'ı istemci içinde benzersiz olmalıdır.soruIdsiçinde yalnızca kendi sorularınızın ID'leri yer alabilir — başka istemcinin sorusu HTTP 400 döner.
// POST https://api.eyena.net/api/embed/odevler
const res = await fetch('https://api.eyena.net/api/embed/odevler', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${access_token}`,
},
body: JSON.stringify({
slug: 'turkce-9-1', // URL'de kullanılır — küçük harf, rakam, tire
baslik: 'Türkçe 9. Sınıf Test 1',
aktif: true,
soruIds: [soru.id, ...], // kendi sorularınızın ID'leri, sıra önemli
}),
})
const odev = await res.json()
// odev.slug ← iframe entegrasyonunda kullanılacak slug// Kendi ödevlerinizi listele
GET https://api.eyena.net/api/embed/odevler
// Tek ödev detayı (sorularıyla birlikte)
GET https://api.eyena.net/api/embed/odevler/{id}
// Güncelle (soru listesini tamamen yeniler)
PUT https://api.eyena.net/api/embed/odevler/{id}
// Body: { slug, baslik, aktif, soruIds }
// Sil
DELETE https://api.eyena.net/api/embed/odevler/{id}Genel Akış
client_secret asla tarayıcıya gönderilmez — tüm gizli adımlar sunucu tarafında kalır.
İframe'e yüklemek istediğiniz ödev ya A bölümündeki CRUD API ile ya da Eyena Sistem Yetkilisi'nin portaldaki "Embed Ödevler" ekranıyla oluşturulmuş ve aktif: trueolmalıdır. Slug'ı not edin — bir sonraki adımda kullanılır.
A bölümündeki Adım 1 ile aynı — tek bir token hem CRUD hem de embed oturumları için geçerlidir.
const res = await fetch('https://giris.eyena.net/connect/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: 'embed-abc123',
client_secret: 'GIZLI_SECRET',
scope: 'api',
}),
})
const { access_token } = await res.json()Access token ile, belirli bir ödev ve öğrenci için kısa ömürlü (2 saat) bir embed token isteyin. Origin başlığı, kayıtlı izinli origin ile birebiraynı olmalıdır. Ödev otomatik olarak token'daki istemciye göre çözümlenir — aynı slug başka bir istemciye ait ise HTTP 404 döner.
const res = await fetch('https://api.eyena.net/api/embed/session', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${access_token}`,
'Origin': 'https://siteniz.com', // kayıtlı izinli origin ile aynı
},
body: JSON.stringify({
slug: 'turkce-9-1', // embed ödev slug'ı (zorunlu)
ogrenciAdi: 'Ali Yılmaz', // öğrenci adı — zorunlu, sonuçlarda geri döner
}),
})
const { embedToken, expiresAt, baslik } = await res.json()
// embedToken'ı frontend'e döndürün (client_secret'ı ASLA döndürmeyin)Origin kayıtlı değilse istek HTTP 403 ile reddedilir. Slug başka bir istemciye aitse HTTP 404 döner.
Embed token'ı kendi backend'inizden alıp iframe'in src'sine yerleştirin. allow="camera; fullscreen" özniteliği, göz izleme kalibrasyonu için zorunludur.
<iframe
id="eyena-frame"
style="width: 100%; height: 600px; border: none;"
allow="camera; fullscreen"
></iframe>
<script>
async function eyenaTestiYukle(slug) {
// 1. Embed token'ı KENDİ backend'inizden isteyin
const res = await fetch('/api/eyena-token?slug=' + slug)
const { embedToken } = await res.json()
// 2. iframe src'sini ayarlayın
document.getElementById('eyena-frame').src =
'https://portal.eyena.net/embed/' + slug + '?token=' + embedToken
}
eyenaTestiYukle('turkce-9-1')
</script>iframe yüklenince size eyena-embed-ready olayı gönderir. Buna karşılık eyena-embed-startkomutunu gönderdiğinizde test, iframe içinde herhangi bir tıklama olmadan başlar — böylece iframe'i gizleseniz bile akışı parent sayfadan yönetebilirsiniz.
const frame = document.getElementById('eyena-frame')
window.addEventListener('message', (event) => {
if (event.origin !== 'https://portal.eyena.net') return
if (event.data?.type === 'eyena-embed-ready') {
frame.contentWindow.postMessage(
{ type: 'eyena-embed-start' },
'https://portal.eyena.net',
)
}
})Göz izleme (Tobii) tam ekran + kamera izni ister; tarayıcılar bunları yalnızca gerçek bir kullanıcı tıklamasından sonra verir. Tamamen gizli/otomatik akış güvenilir biçimde yalnızca göz izleme olmayan testlerde çalışır.
Öğrenci testi tamamlayınca iframe, postMessageile parent sayfaya iki olay gönderir. Payload'a güvenmeden önce her zaman event.origin'u doğrulayın.
window.addEventListener('message', (event) => {
if (event.origin !== 'https://portal.eyena.net') return
if (event.data?.type === 'eyena-embed-result') {
const { schemaVersion, ogrenciAdi, answers, cards, regions, gaze, heatmaps } = event.data
// answers: [{ soruId, secenekId }] (secenekId boşsa null)
// cards: [{ soruId, image, width, height, scale, coordSpace: 'card' }]
// image: OPAK JPEG data URL — öğrencinin gördüğü kartın TAMAMI
// (soru + görsel + şıklar). Diğer tüm katmanlar bu kutuya göre ölçülür.
// regions: [{ soruId, kind: 'soru'|'secenek', secenekId, x, y, w, h }]
// gaze: [{ soruId, coordSpace, points: [{ x, y, timestamp, secenekId }] }]
// heatmaps: [{ soruId, image, width, height }] (şeffaf PNG, veri yoksa null)
// Arka plan olarak cards[].image kullanın — soru görselini (icerik) DEĞİL.
// icerik şıkları içermez ve kartla aynı kutu değildir; üzerine bindirilen
// heatmap hizalanmaz, şıklara bakılan süre görselin dışında kalır.
const card = cards.find((c) => c.soruId === soruId)
canvas.width = card.width // CSS px — koordinat kutusu
canvas.height = card.height
ctx.drawImage(cardImg, 0, 0, card.width, card.height)
ctx.drawImage(heatmapImg, 0, 0, card.width, card.height)
console.log('Sonuç:', event.data)
}
if (event.data?.type === 'eyena-embed-complete') {
console.log('Test tamamlandı:', event.data.slug)
}
})Göz izleme verisi (gaze) yalnızca Tobii aktifken ve iframe'e kamera/tam ekran izni verildiğinde dolar. Aksi halde test yine çalışır ama gaze dizisi boş, heatmaps[].image alanları null gelir.cards[].imagegöz izlemeden bağımsızdır ve her durumda gelir. Kart görselleri, öğrenci sorudan ayrılırken (son soru için Tamamla'da) alınır — seçtiği şık dahil, bıraktığı hâliyle. Çizimleri görsele işlenmez; ayrı olarak drawings alanında gelir. Tüm görseller tamamen tarayıcıda üretilir ve postMessage ile aktarılır — sunucuya hiçbir görsel kaydedilmez.
postMessagepayload'ı yalnızca ham veri taşır — soru görseli iframe'den geçmez. İçeriği kendi tarafınızda zaten tutuyorsanız bu adım genellikle gereksizdir; demo/önizleme senaryoları için aynı embed token'ı kullanabilirsiniz.
// Sunucu tarafı — embed token'ı query string'de geçirin
const res = await fetch(
'https://api.eyena.net/api/embed/coz?token=' + encodeURIComponent(embedToken),
{ cache: 'no-store' },
)
const detay = await res.json()Yanıt gövdesi:
{
"slug": "turkce-9-1",
"baslik": "Türkçe 9. Sınıf - 1. Ünite",
"ogrenciAdi": "Ali Yılmaz",
"sorular": [
{
"id": "a1b2…", // postMessage'taki soruId ile eşleşir
"sira": 1,
"baslik": "1. Soru",
"icerik": "https://…", // blob referansı çözülmüş URL
"secenekler": [
{ "id": "c1…", "metin": "Birinci şık", "sira": 1 },
{ "id": "c2…", "metin": "İkinci şık", "sira": 2 }
],
"bolgeler": [
{ "x": 10, "y": 20, "genislik": 200, "yukseklik": 150,
"aciklama": "Grafik", "sira": 1 }
]
}
]
}Token kendisi yetkilendirmedir — ayrıca access token gerekmez. Geçersiz/süresi dolmuş token HTTP 401, istemciye ait olmayan slug HTTP 404 döner.
client_secret ve access_token asla frontend kodunda yer almamalı.| Yöntem | Endpoint | Açıklama | Auth |
|---|---|---|---|
| POST | /connect/token | M2M access token al | client_credentials |
| POST | /api/embed/session | Embed (iframe) token al | Bearer access_token |
| GET | /api/embed/coz?token=… | Ödev detayını al | embed token (query) |
| GET | /api/embed/sorular | Soruları listele | Bearer access_token |
| POST | /api/embed/sorular | Soru oluştur (multipart/form-data) | Bearer access_token |
| GET | /api/embed/sorular/{id} | Soru detayı | Bearer access_token |
| PUT | /api/embed/sorular/{id} | Soru güncelle (multipart/form-data) | Bearer access_token |
| DELETE | /api/embed/sorular/{id} | Soru sil (görsel de silinir) | Bearer access_token |
| GET | /api/embed/odevler | Ödevleri listele | Bearer access_token |
| POST | /api/embed/odevler | Ödev oluştur | Bearer access_token |
| GET | /api/embed/odevler/{id} | Ödev detayı | Bearer access_token |
| PUT | /api/embed/odevler/{id} | Ödev güncelle | Bearer access_token |
| DELETE | /api/embed/odevler/{id} | Ödev sil | Bearer access_token |
/connect/token ve /api/embed/session istekleri https://giris.eyena.net / https://api.eyena.net adresleri üzerinden gönderilir. Diğer tüm endpointler https://api.eyena.net altındadır.