خطافات الويب
ترسل خطافات الويب طلب POST عبر HTTPS إلى خادمك عند انتهاء عملية توليد، فلا تحتاج إلى الاستعلام المتكرر.
إضافة نقطة استقبال
أضف نقاط الاستقبال من صفحة حسابك، في قسم المطوّرين: عنوان https على المنفذ الافتراضي، والأحداث التي تريدها، واختياريًا مفتاح واحد لواجهة البرمجة تستقبل عمليات التوليد الخاصة به فقط. تحصل على سر توقيع يبدأ بـ whsec_ ويظهر مرة واحدة.
الأحداث
| الحدث | يُرسَل عندما |
|---|---|
generation.succeeded | تنتهي عملية توليد وتصبح مخرجاتها جاهزة. |
generation.failed | تفشل عملية توليد؛ وقد استُردت عملاتها. |
generation.canceled | تُلغى عملية توليد كانت في الطابور. |
الطلب
المتن بصيغة JSON ويحتوي على id الحدث وtype وcreatedAt وdata.generation: عملية التوليد كما تعيدها GET /v1/generations/:id، مع روابط تنزيل جديدة.
Inker-Signature: التوقيع:t=وقت يونكس بالثواني وv1=قيمة HMAC-SHA256 بالنظام الست عشري.Inker-Event-Id: معرّف الحدث، وهو نفسه في كل إعادة محاولة. استخدمه لتجاهل التكرارات.Inker-Event-Type: نوع الحدث.
POST /webhooks/inker HTTP/1.1
Content-Type: application/json
Inker-Signature: t=1791200000,v1=5f0c…e81a
Inker-Event-Id: EVENT_ID
Inker-Event-Type: generation.succeeded
{
"id": "EVENT_ID",
"type": "generation.succeeded",
"createdAt": "2026-10-05T12:00:00.000Z",
"data": {
"generation": {
"id": "…",
"modelId": "…",
"status": "succeeded",
"coinsQuoted": 12,
"coinsCharged": 12,
"outputs": [{ "id": "…", "kind": "image", "url": "https://cdn.inker.si/…" }]
}
}
}التحقق من التوقيع
احسب قيمة HMAC-SHA256 بالنظام الست عشري للطابع الزمني متبوعًا بنقطة ثم متن الطلب الخام، باستخدام سر التوقيع كاملًا مفتاحًا. قارنها في زمن ثابت بكل قيمة v1، وارفض أي طابع زمني يبعد عن ساعتك أكثر من 300 ثانية. استخدم دائمًا البايتات نفسها التي استلمتها، قبل أي تحليل لـ JSON.
import { createHmac, timingSafeEqual } from 'node:crypto';
const TOLERANCE_SECONDS = 300;
/**
* header: the Inker-Signature header ("t=<unix seconds>,v1=<hex>").
* rawBody: the request body exactly as received (string or Buffer), before JSON parsing.
*/
export function verifyInkerSignature(secret, header, rawBody, nowMs = Date.now()) {
if (!header) return false;
let timestamp = null;
const signatures = [];
for (const part of header.split(',')) {
const [name, value] = part.trim().split('=', 2);
if (!value) continue;
if (name === 't' && /^\d{1,12}$/.test(value)) timestamp = Number(value);
if (name === 'v1' && /^[0-9a-f]{64}$/.test(value)) signatures.push(value);
}
if (timestamp === null || signatures.length === 0) return false;
if (Math.abs(Math.floor(nowMs / 1000) - timestamp) > TOLERANCE_SECONDS) return false;
const expected = createHmac('sha256', secret)
.update(`${timestamp}.`)
.update(rawBody)
.digest();
return signatures.some((signature) => timingSafeEqual(Buffer.from(signature, 'hex'), expected));
}import express from 'express';
const app = express();
const seen = new Set(); // use your database in production
// express.raw keeps the exact bytes the signature covers.
app.post('/webhooks/inker', express.raw({ type: 'application/json' }), (req, res) => {
const ok = verifyInkerSignature(
process.env.INKER_WEBHOOK_SECRET,
req.get('Inker-Signature'),
req.body,
);
if (!ok) return res.status(400).send('bad signature');
const eventId = req.get('Inker-Event-Id');
if (seen.has(eventId)) return res.sendStatus(200); // a retry of an event you handled
seen.add(eventId);
const event = JSON.parse(req.body.toString('utf8'));
if (event.type === 'generation.succeeded') {
// event.data.generation.outputs[].url: download soon, links are short-lived
}
res.sendStatus(200); // answer 2xx within 10 seconds; do slow work afterwards
});import hashlib
import hmac
import re
import time
TOLERANCE_SECONDS = 300
def verify_inker_signature(secret: str, header: str | None, raw_body: bytes, now: float | None = None) -> bool:
"""header: the Inker-Signature header; raw_body: the request body exactly as received."""
if not header:
return False
timestamp = None
signatures = []
for part in header.split(","):
name, _, value = part.strip().partition("=")
if name == "t" and re.fullmatch(r"[0-9]{1,12}", value):
timestamp = int(value)
elif name == "v1" and re.fullmatch(r"[0-9a-f]{64}", value):
signatures.append(value)
if timestamp is None or not signatures:
return False
current = int(time.time() if now is None else now)
if abs(current - timestamp) > TOLERANCE_SECONDS:
return False
expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(signature, expected) for signature in signatures)إعادة المحاولة
أجب بأي حالة 2xx خلال 10 ثوانٍ. وإلا يعيد inker المحاولة بعد 30 ث و2 د و10 د و30 د و1 س و3 س و6 س و12 س: 9 محاولات على مدى يوم تقريبًا، ثم يُعلَّم الإرسال على أنه فاشل. قد يصل الإرسال نفسه أكثر من مرة وبغير ترتيب، لذا أزل التكرار اعتمادًا على معرّف الحدث.
إذا كان السر قد تسرّب، فجدّده من صفحة حسابك: يتوقف السر القديم عن العمل فورًا. وتجد هناك أيضًا عمليات الإرسال الأخيرة لكل نقطة استقبال مع رموز الحالة والأخطاء.