CodeGym /Kurslar /ChatGPT Apps /Webhook-lar və xarici inteqrasiyalar: imza, taymautlar, i...

Webhook-lar və xarici inteqrasiyalar: imza, taymautlar, idemponentlik

ChatGPT Apps
Səviyyə, Dərs
Mövcuddur

1. ChatGPT App-də webhook-lar: ümumiyyətlə kim kimə müraciət edir

Klassik HTTP dünyasında hər şey sadədir: siz — müştəri, POST /api/... edirsiniz, server cavab verir, hamı xoşbəxtdir. Webhook-larda isə əksinədir: xarici servis kənarda nəsə baş verəndə HTTP sorğunu sizin backend-ə özü göndərir.

ChatGPT Apps ekosistemində bu bir neçə tipik ssenaridə üzə çıxır. Məsələn, GiftGenius ACP/Instant Checkout vasitəsilə checkout yaratdıqdan sonra ödəniş provayderindən webhook ilə payment_succeeded bildirişi alır. Və ya hədiyyələr üçün önizləmə şəkilləri yaradan fon xidməti render bitəndə sizə image_ready göndərir. Belə hallarda ChatGPT və sizin MCP server artıq işini görüb, top qarşı tərəfdədir və o, nəticəni sizə webhook ilə bildirir.

Açar xüsusiyyət: təşəbbüs sizin sistemin xaricindədir. Sorğu istənilən an və istənilən sayda gələ bilər. Buna görə də webhook işləyicisini potensial ən həssas nöqtə kimi düşünmək lazımdır — ora bütün internet “döyür”.

Kontrast üçün kiçik cədvəl:

Çağırış növü Kim başlayır GiftGenius-da nümunə
Adi API sorğusu Siz MCP server Stripe API-ni çağırır
Webhook Xarici sistem Stripe sizə payment_succeeded göndərir

2. Sadə sxem: harada ChatGPT, harada MCP, harada webhook

Sxematik olaraq yol belə görünür:

sequenceDiagram
    participant User as İstifadəçi ChatGPT-də
    participant GPT as ChatGPT + model
    participant App as GiftGenius (MCP/App)
    participant PSP as Ödəmə xidməti (Stripe/ACP)

    User->>GPT: "Hədiyyə almaq istəyirəm"
    GPT->>App: callTool(create_checkout)
    App->>PSP: POST /checkout_sessions
    PSP-->>App: 200 OK + checkout_session_id
    App-->>GPT: ToolOutput (checkout məlumatı)

    PSP-->>App: POST /webhooks/payment_succeeded
    App-->>PSP: 200 OK (hadisəni qəbul etdik)
    App->>DB: sifarişi ödənilmiş kimi işarələmək

Birinci hissə — sizin artıq bacardığınız, adi çıxan sorğulardır. Webhook — sxemin aşağı hissəsidir, burada ödəniş xidməti özü sizə müraciət edir. Bu gün bizi maraqlandıran məhz budur.

3. Next.js-də əsas webhook işləyicisi (skelet)

Biz tədris layihəmiz olan GiftGenius-u Next.js 16 üzərində inkişaf etdirməyə davam edirik. Şablonda app/ ilə UI və app/mcp/route.ts ilə MCP server var.

Webhook işləyicisini ayrıca HTTP marşrutuna çıxarmaq məntiqlidir, məsələn: app/api/webhooks/commerce/route.ts.

Minimal karkas belə görünür:


// app/api/webhooks/commerce/route.ts
import { NextRequest } from "next/server";

export async function POST(req: NextRequest) {
  const rawBody = await req.text();          // 1. Gövdəni mətn kimi oxuyuruq
  const headers = Object.fromEntries(req.headers); // 2. Başlıqları götürürük

  // 3. TODO: imzanın yoxlanması (bir azdan əlavə edəcəyik)
  // 4. TODO: JSON-u pars etmək və hadisəni emal etmək

  return new Response("ok", { status: 200 }); // 5. Sürətli 2xx cavabı qaytarırıq
}

Burada artıq bir neçə mühüm ideya gizlidir.

Birincisi, biz gövdəni mətn kimi oxuyuruq, birbaşa await req.json() deyil. Bir çox provayderlər məhz gövdənin “xam” bayt axınını imzalayır və əgər siz onu imzadan əvvəl pars etsəniz (xüsusən formatlasanız), imza artıq uyğun gəlməyəcək.

İkincisi, biz dərhal sürətli 2xx cavırı haqqında düşünürük. Ağır işi ya ayrıca worker-ə, ya da heç olmasa hadisəni log-ladıqdan sonra async funksiyaya çıxarmaq daha yaxşıdır. Bu, birbaşa taymautlar və təkrar göndərişlərlə bağlıdır, bu barədə bir azdan danışacağıq.

4. Webhook imzası: “Stripe”‑i “curl ilə gələn kəsdən” necə fərqləndirmək

Webhook işləyicisi skeletindəki TODO-nu xatırlayaq — “imzanın yoxlanması”. Gəlin, real Stripe-i “curl ilə gələn kəsdən” dəqiq necə ayırd edəcəyimizi başa düşək.

Ən sadəlövhlük — URL mürəkkəbdirsə (/api/webhooks/stripe/super-secret-abc123), deməli, heç kim onu tapmayacaq kimi düşünməkdir. URL sirləri mahiyyətcə security through obscurity-dir: mürəkkəb URL arxasında gizlənmək cəhdi çox zəif qoruma verir. Düzgün müdafiə xətti kriptoqrafik imzadır.

Demək olar bütün ciddi provayderlər (Stripe, ACP, bir çox CRM) sorğu gövdəsi və zaman əsasında HMAC-imza hesablayır və nəticəni başlığa qoyur. Siz, qəbul edən tərəf kimi, eyni şeyi edirsiniz və müqayisə edirsiniz. Nə qədər cüzi uyğunsuzluq olsa — sorğunu saxtakarlıq kimi atırsınız.

Ümumi resept:

  1. Sizdə provayder kabinetindən aldığınız və mühit sirlərinə qoyduğunuz webhook sirri var (məsələn, Vercel env-də STRIPE_WEBHOOK_SECRET).
  2. Provayder sorğunu göndərərkən timestamp + '.' + rawBody üzrə HMAC hesablayır.
  3. Başlığa, məsələn Stripe-Signature-a, timestamp və bir və ya bir neçə imza yazır.
  4. Siz işləyicidə timestamp-i götürür, eyni qayda ilə öz HMAC-ınızı hesablayır və müqayisə edirsiniz.

TypeScript-də crypto-dan istifadə ilə mini-nümunə:

import crypto from "crypto";

function computeSignature(secret: string, payload: string) {
  return crypto
    .createHmac("sha256", secret)  // alqoritmi seçirik
    .update(payload, "utf8")       // gövdənin xam mətni
    .digest("hex");                // hex sətir
}

İmzanın və hadisənin təzəliyinin yoxlanması nümunəsi:

const sigHeader = headers["stripe-signature"];
if (!sigHeader) return new Response("missing signature", { status: 400 });

const [tsPart, sigPart] = sigHeader.split(",").map(s => s.trim());
const timestamp = Number(tsPart.split("=")[1]);
const theirSig = sigPart.split("=")[1];

const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > 5 * 60) {
  return new Response("timestamp too old", { status: 400 });
}

const payload = `${timestamp}.${rawBody}`;
const expectedSig = computeSignature(
  process.env.STRIPE_WEBHOOK_SECRET!,
  payload
);

if (!crypto.timingSafeEqual(
  Buffer.from(expectedSig, "hex"),
  Buffer.from(theirSig, "hex")
)) {
  return new Response("invalid signature", { status: 400 });
}

timingSafeEqual-ə diqqət yetirin — bu, vaxt üzrə hücumlardan qorunmadır; yəni hücumçu müqayisə müddətinə görə imzanı “tutmağa” çalışa bilər.

İmzanı uğurla yoxladıqdan sonra artıq arxayınlıqla JSON.parse(rawBody) və ya await req.json() edə bilərsiniz — bunun real provayderdən gəldiyini bilirsiniz.

IP allowlist (yalnız provayder ünvanlarından sorğulara icazə vermək) və webhook-lar üçün ayrıca domen kimi əlavə müdafiə səviyyələri də pis deyil, amma məhz kriptoimza sizə orijinallığa inam verir.

5. Taymautlar, sürətli cavab və asinxron emal

Webhook-lar tez cavab verənləri “sevir”. Əksər ödəniş və commerce platformaları endpoint-inizin bir neçə saniyə ərzində 2xx cavabı qaytarmasını gözləyir (tez-tez 10 saniyəyədək, bəzən daha az). Əgər siz “çox düşünürsünüzsə”, onlar çağırışı uğursuz sayır və sorğuları təkrar göndərməyə başlayırlar.

Birbaşa yanaşma belə görünür: imzanı yoxladınız, DB-yə baxdınız, daha üç xarici API-ə getdiniz, hesabat hesabladınız, PDF yaratdınız və yalnız sonra 200 OK qaytardınız. Bunlardan nəsə azacıq ilişsə, ödəniş xidməti webhook-un düşdüyünü düşünüb onu yenidən göndərəcək. Nəticədə siz sifarişi iki dəfə yaradacaq, məktubu iki dəfə göndərəcək, hansısa GPT alətini iki dəfə çağıracaqsınız — və xaosu düzəltməyə qaçacaqsınız.

Düzgün pattern “qəbul et, yaz, təxirə sal” kimi səslənir:

  1. Imzanı və bazı invariyantları (hadisə tipi, məcburi sahələr) yoxlamaq.
  2. Hadisəni cədvələ/növbəyə sürətlə yazmaq (minimum DB əməliyyatı).
  3. 2xx qaytarmaq.
  4. Hadisəni fon rejimində, ayrı worker ilə emal etmək.

Ayrı növbə olmadan, amma sürətli fiksasiya ilə “yarım-düzgün” işləyici nümunəsi:

export async function POST(req: NextRequest) {
  const rawBody = await req.text();
  const headers = Object.fromEntries(req.headers);

  if (!verifySignature(headers, rawBody)) {
    return new Response("invalid signature", { status: 400 });
  }

  const event = JSON.parse(rawBody);
  await saveWebhookEvent(event); // DB-yə sürətli yazma

  // Burada tapşırığı setImmediate/queue vasitəsilə fon emalına göndərə bilərsiniz,
  // amma tədris nümunəsində hələlik yalnız yazı ilə kifayətlənirik: await olmadan çağırırıq,
  // 200 cavabının dərhal getməsi üçün.
  processWebhookEventLater(event).catch(console.error);

  return new Response("ok", { status: 200 });
}

Diqqət edin: biz await ilə processWebhookEventLater(...) çağırmırıq. İşləyici tapşırığı fon rejiminə qoyur və taymautlara ilişməmək üçün dərhal 200 qaytarır.

Real production-da bu yerdə çox vaxt növbə yaranır (məsələn, ayrıca webhook_jobs cədvəli və ya xarici servis), worker-lər isə hadisələri səliqə ilə emal edərək yeni qəbulun qarşısını almır.

6. Idemponentlik və deduplikasiya: pulların iki dəfə silinməsinin qarşısını necə almaq

Tədris nümunələrində ideal oxlar çəkilir: bir hadisə → bir emal → xoşbəxt sifariş. Real həyatda isə webhook-lar “yumşaq pişiklər” kimidirlər — paketlərlə və bir neçə dəfə ard-arda gəlirlər.

Səbəblər sadədir: şəbəkə etibarlı deyil, taymautlar baş verir və bir çox provayderlər dizayn etibarilə 2xx cavabını dəqiq alana qədər hadisələri təkrar göndərirlər. Xüsusilə ödənişlərdə bu önəmlidir: payment_succeeded-i itirməkdənsə, onu yenidən göndərmək daha yaxşıdır.

Ona görə biznes məntiqiniz idemponent olmalıdır: eyni hadisənin təkrar emalı nəticəni dəyişməməlidir (və ya ən azı sistemi sındırmamalıdır).

Tipik pattern:

  1. Hadisənin dayanıqlı identifikatoru var, məsələn event.id və ya checkout_session_id.
  2. Siz onu emal edilmiş hadisələr cədvəlində saxlayırsınız və həmin sahəyə unikal indeks qoyursunuz.
  3. Hər webhook-da əvvəlcə yoxlayırsınız: əgər eyni id ilə “emal edilib” statuslu yazı artıq varsa, sadəcə 200 cavablayın və heç nə etməyin.

Psixdo-ORM ilə mini-nümunə:

async function handlePaymentSucceeded(event: any) {
  const existing = await db.webhookEvents.findUnique({
    where: { providerId: event.id },
  });
  if (existing?.processedAt) {
    return; // artıq hər şey edilib
  }

  await db.$transaction(async (tx) => {
    await tx.webhookEvents.upsert({
      where: { providerId: event.id },
      update: { processedAt: new Date() },
      create: {
        provider: "stripe",
        providerId: event.id,
        type: event.type,
        payload: event,
        processedAt: new Date(),
      },
    });

    await tx.orders.update({
      where: { checkoutSessionId: event.data.object.id },
      data: { status: "PAID" },
    });
  });
}

Burada tranzaksiya vacibdir: siz hadisəni eyni anda “emal edilib” kimi nişanlayır və sifarişi dəyişirsiniz. Hər şey ortada düşsə, tranzaksiya geri qayıdar və növbəti webhook retranslyasiyasında yenidən cəhd edərsiniz — ikiqat yazı olmadan.

Yaxşı təcrübə həmçinin əməliyyatların özlərini də idemponent etməkdir, məsələn:

  • “sifariş statusunu PAID etmək” “balansı +100 artırmaq” əvəzinə;
  • “yazı yoxdursa, yaratmaq” “daha bir sətir əlavə etmək” əvəzinə.

7. Webhook məlumatlarının validasiyası və PII: imza — yeganə filtr deyil

Webhook imzalanmış və real servisdən gəlmiş olsa belə, onun məlumatlarına istifadəçi girişi və ya alət arqumentləri kimi eyni şübhə ilə yanaşmaq lazımdır. Keçən mühazirədə artıq müzakirə etmişdik ki, sxemlər və normallaşdırma sizin firewall-ınızdır.

Hadisə üçün sxem, məsələn, aşağıdakı kimi ola bilər (TypeScript/Zod səviyyəsində):

import { z } from "zod";

const paymentSucceededSchema = z.object({
  id: z.string(),
  type: z.literal("payment_succeeded"),
  data: z.object({
    object: z.object({
      id: z.string(),            // checkout_session_id
      amount_total: z.number(),
      currency: z.string(),
      metadata: z.record(z.string(), z.string()).optional(),
    }),
  }),
});

İşləyicidə siz valide edirsiniz:

const event = JSON.parse(rawBody);
const parsed = paymentSucceededSchema.parse(event);
// sonra yalnız parsed ilə işləyirsiniz

Beləcə, “provayder formatı dəyişdi”, “test mühitində sahə nullable oldu” və s. kimi sürprizlərdən qorunursunuz. Nəsə qaydasında deyilsə — loglarda səhvi qeyd edin və 400 qaytarın; provayder sonra təkrar göndərəcək və ya xəbərdarlıq yollayacaq.

PII-ni də unutmayaq: webhook gövdələri tez-tez email, çatdırılma ünvanı, hətta bəzən ödəniş məlumatlarının hissələrini (tokenləşdirilmiş formada) ehtiva edir. Onları loglarda maskalamaq və xam şəkildə üçüncü tərəf APM/log servislərinə göndərməmək — gizlilik mövzusunda mütləq praktika, bunu sirlər və məxfi məlumatlar mövzusunda müzakirə etmişdik.

Və əlbəttə ki, webhook-un tam JSON-unu filtrsiz şəkildə ChatGPT-ə ToolOutput kimi göndərməyə ehtiyac yoxdur — model provayderin göndərdiyi hər şeyi görməməlidir, xüsusən UX üçün lazım deyilsə.

8. GiftGenius praktikada: ACP/Instant Checkout üçün ödəniş webhook-u

GiftGenius-ə qayıdaq. Kommersiya və ACP bölməsində agentin checkout sessiyası yaratmasını və daha sonra Instant Checkout vasitəsilə ödənişin silinməsini artıq müzakirə etmişdik. Backend baxımından bundan sonra order.paid webhook-u (və ya Stripe terminlərində checkout.session.completed) gözləmək qalır ki:

  • sifarişin statusunu təsdiqləyəsiniz;
  • “məktub göndər” / “göndərişi hazırla” zəncirini işə salasınız;
  • agenta “ödəmə keçdi” deyə etibarlı cavab verəsiniz.

Next.js-də sadə işləyici nümunəsi:

// app/api/webhooks/commerce/route.ts
import { NextRequest } from "next/server";
import { handlePaymentSucceeded } from "@/lib/webhooks/commerce";

export async function POST(req: NextRequest) {
  const rawBody = await req.text();
  const headers = Object.fromEntries(req.headers);

  if (!verifyCommerceSignature(headers, rawBody)) {
    return new Response("invalid signature", { status: 400 });
  }

  const event = JSON.parse(rawBody);
  if (event.type === "payment_succeeded") {
    // Əvvəlki bölmədəki idemponent işləyici
    await handlePaymentSucceeded(event);
  }

  return new Response("ok", { status: 200 });
}

verifyCommerceSignature funksiyası yuxarıda baxdığımız HMAC-imza məntiqini həyata keçirir. Real layihədə hər provayder üçün ayrıca modul etmək məntiqlidir (verifyStripeSignature, verifyACPCheckoutSignature), sxemləri qarışdırmamaq üçün.

handlePaymentSucceeded daxilində siz:

  • obyekti sxemə görə (Zod) valide edirsiniz;
  • tranzaksiyada hadisəni “emal edilib” kimi qeyd edir və sifarişi yeniləyirsiniz;
  • istəyə görə “yavaş” əməliyyatlar üçün növbəyə tapşırıq qoyursunuz: məktublar, analitika, əlavə API çağırışları.

Bu yanaşma “ACP → webhook → GiftGenius” zəncirini təkrar hadisələrə, müvəqqəti nasazlıqlara və qəribə məlumatlara qarşı dayanıqlı edir.

9. Webhook-lar MCP, ChatGPT və alətlərlə harada qovuşur

İlk baxışda elə görünür ki, webhook-lar ChatGPT App-dən ayrı yaşayır: backend-də hansısa HTTP marşrutu vəssalam. Əslində bu, ümumi arxitekturanın vacib hissəsidir.

Adətən bağ belə görünür:

  1. MCP aləti create_checkout ChatGPT-də model tərəfindən çağırılır.
  2. MCP server ödəniş xidmətinə müraciət edir, checkout sessiyası yaradır və ToolOutput-da sifariş haqqında məlumat və “ödəmə gözlənilir” statusunu qaytarır.
  3. İstifadəçi ödənişi UI-da tamamlayır (Instant Checkout bunu birbaşa ChatGPT-də edir).
  4. Ödəniş xidməti sizin backend-ə webhook göndərir.
  5. Backend DB vasitəsilə sifarişin statusunu dəyişir; alətlərə növbəti çağırışda və ya modeldən follow‑up gələndə artıq dürüst deyə bilərsiniz: “Sifariş ödənildi, detalları budur”.

Bəzən backend dolayı yolla follow‑up başlada bilər — məsələn, serverdən gələn siqnalla özü sendFollowUpMessage-i çağıran vidcet və ya Realtime inteqrasiyası vasitəsilə. Amma bu olmasa belə, ödənişin faktı sizdə saxlanılır və alətin növbəti çağırışında backend statusu DB-dən oxuyub modelə cavab üçün yenilənmiş məlumatları qaytarır.

Vacibdir ki, webhook — MCP serverlə eyni səviyyədə yaşayan giriş nöqtəsidir və eyni servislərdən (DB, növbələr, sirlər) istifadə edir. Təhlükəsizlik məntiqi mahiyyətcə eynidir: minimal səlahiyyətlər, validasiya olunmuş giriş məlumatları, səliqəli loglama.

10. Webhook-lar və xarici inteqrasiyalarla işləyərkən tipik səhvlər

Səhv №1: webhook imzasının yoxlanmaması.
Bəzən inkişafçılar “gizli” URL və ya başlıqda sadə Bearer my-secret ilə kifayətlənirlər. Əgər bu sirr haradasa sızsa, istənilən kəs sizə webhook-lar “çırpa”, sifarişlər yarada, ödəniş statuslarını dəyişə və ümumiyyətlə istədiyini edə bilər. Düzgün yanaşma — gövdənin kriptoqrafik imzası (HMAC) və timestamp yoxlamasıdır. Bu, “URL seçmək”dən qat-qat çətin saxtalaşdırma yaradır.

Səhv №2: ağır emalı webhook sorğusunun içində etmək.
Webhook işləyicisində “sifariş yarat, iki xarici API-ə get, PDF generasiya et, GPT modeli çağır, 5 məktub göndər” yazmaq taymaut və təkrar cəhdləri əminliklə qarşılamaq üsuludur. Nəticədə elə özünüz dublikatlar yaradırsınız və sonra onları açmağa çalışırsınız. Daha etibarlısı hadisənin qəbulunu tez təsdiqləməkdir (2xx), onu DB-yə və ya növbəyə yazmaq və fon rejimində emal etmək.

Səhv №3: idemponent olmayan biznes məntiqi.
Tez-tez “hər payment_succeeded-də balansa məbləği artır” tipli kod görmək olur. Webhook iki dəfə gəlsə, balans ikiqat artacaq. Başqa variant — eyni sifarişi iki dəfə yaratmaq və ya istifadəçiyə məktubu iki dəfə göndərmək. Idemponentlik hadisənin dayanıqlı identifikatoru, emal edilmiş hadisələr cədvəli, tranzaksiyalar və “statusu təyin et” tipli əməliyyatlarla əldə olunur, “daha da əlavə et” əvəzinə.

Səhv №4: webhook məlumatları üçün sxem və validasiyanın olmaması.
İmzalanmış webhook belə sizin gözlədiyiniz kimi olmaya bilər: provayder formatı dəyişdi, siz JSON-u sənədləşmədən kopyaladınız, amma test mühitində sahə başqa cür adlanır və ya sadəcə tiplərdə yanıldınız. Belə JSON-u sxemsiz və yoxlamasız emal etsəniz, səhvlər sakitcə sifarişləri sındıracaq və ya zəncirin ortasında istisnalar doğuracaq. Girişdə Zod/JSON Schema istifadə etmək diaqnostikanı sadələşdirir və qeyri-valid hadisələri dəqiq kənarlaşdırmağa imkan verir.

Səhv №5: webhook-ların xam gövdələrini PII ilə loglamaq.
Debug həvəsi ilə console.log(rawBody) qoymaq və onu unutmaq asandır. Production-da bu, email-larla, ünvanlarla və digər PII ilə dolu loglara çevrilir, üstəlik üçüncü tərəf log servislərinə gedir. Məxfilik və tənzimləmə (GDPR bənzəri) baxımından bu, birbaşa “ayağa atəş”dir. Daha yaxşısı dərhal PII-scrub tətbiq etməkdir — həssas sahələri maskalamaq və diaqnostika üçün həqiqətən lazım olanları loglamaq.

Səhv №6: test və prod webhook-larını qarışdırmaq.
Tipik vəziyyət — eyni endpoint həm provayderin test, həm də prod mühitindən hadisələri qəbul edir. Nəticədə test ödənişi qəfildən real sifarişin statusunu dəyişir və ya əksinə. URL-ləri ayırmaq daha etibarlıdır (məsələn, /webhooks/commerce/test və /webhooks/commerce/live) və ya heç olmasa konfiqdə “rejim” saxlamaq və girişdə onu yoxlamaq.

Səhv №7: ChatGPT ssenarisinin sinxron webhook-dan tam asılılığı.
Bəzən aləti çağırdıqdan və checkout sessiyası yaratdıqdan sonra modelin dərhal ödəniş nəticəsini bilməsi istənilir. Amma webhook-lar tərifcə asinxrondur və ödəniş vaxt apara bilər. Hər şeyin dərhal baş verəcəyi kimi ssenari qurmaq pis fikirdir. Dialoqları və alətləri elə layihələndirmək daha yaxşıdır ki, onlar təxirə salınmış hadisələrlə düzgün yaşasınlar: sifarişin vəziyyətini saxlamaq, istifadəçiyə çata qayıtmağa və daha sonra aktual məlumat almağa imkan vermək.

1
Tapşırıq
ChatGPT Apps, səviyyə, dərs
Bağlanıb
Minimal imzalı webhook (HMAC + zaman damğası)
Minimal imzalı webhook (HMAC + zaman damğası)
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION