# Verifikasi tanda tangan webhook formulir

> Pastikan webhook formulir benar-benar berasal dari Ikiform: skema HMAC-SHA256 Standard Webhooks, test vector, body mentah, timestamp, dan rotasi secret.

Terbit 2026-10-25 · https://ikiform.com/id/guides/webhook-signature-verification

Hitung ulang tanda tangan sendiri dan bandingkan dengan yang ada di request. Ikiform mengikuti Standard Webhooks: tanda tangannya adalah HMAC-SHA256 base64 dari `webhook-id`, `webhook-timestamp`, dan body mentah yang digabung dengan titik, dengan key berupa bagian hasil decode base64 dari secret `whsec_` Anda, dan dikirim sebagai `v1,<signature>`. Terima request hanya jika satu tanda tangan `v1` cocok dalam perbandingan constant-time dan timestamp berada dalam 5 menit.

Endpoint webhook adalah URL publik yang menerima request POST dari siapa pun. Tanpa pemeriksaan tanda tangan, siapa pun yang tahu URL-nya bisa mengirim kiriman palsu ke CRM, database, atau chat tim Anda. Pemeriksaannya hanya beberapa baris kode, dan menulis baris-baris itu dengan tepat adalah inti panduan ini.

## Cara kerja penandatanganan

Ikiform dan server Anda berbagi satu secret, yang ditampilkan sekali saat Anda menambahkan endpoint. Sebelum setiap percobaan pengiriman, Ikiform meng-hash request dengan secret itu dan menaruh hasilnya di header. Server Anda mengulang perhitungan itu pada apa yang diterimanya. Hanya pihak yang memegang secret yang bisa menghasilkan nilai yang cocok, dan perubahan apa pun pada body, id, atau timestamp akan merusaknya.

Diagram: Bagaimana Ikiform menandatangani pengiriman webhook dan bagaimana server Anda memeriksanya

1. Ikiform menserialisasi event sebagai JSON.
2. Ikiform menggabungkan id pengiriman, Unix timestamp percobaan, dan body dengan titik.
3. Ikiform menghitung HMAC-SHA256 dari string itu dengan setiap secret aktif milik endpoint.
4. Ikiform mengirim setiap hasil, di-encode base64 dan diberi awalan v1, di header webhook-signature.
5. Handler Anda membaca body mentah dan ketiga header sebelum mem-parsing apa pun.
6. Handler menghitung HMAC yang sama dengan salinan secret-nya.
7. Handler membandingkan secara constant-time dengan setiap tanda tangan v1 dan memeriksa timestamp berada dalam 5 menit.
8. Jika cocok, handler menjawab 2xx dan menangani event. Jika tidak, handler menjawab 400 dan tidak melakukan apa pun.

Tidak ada yang rahasia ikut terkirim bersama request. Tanda tangan membuktikan bahwa pengirim tahu secret-nya, dan bahwa byte-nya tidak diubah di perjalanan.

## Skemanya, secara persis

Ikiform memakai [Standard Webhooks](https://www.standardwebhooks.com), spesifikasi terbuka yang dipakai bersama oleh beberapa penyedia webhook. Setiap pengiriman membawa tiga header:

| Header | Nilai | Catatan |
| --- | --- | --- |
| `webhook-id` | `dlv_...` | Id pengiriman. Sama di setiap percobaan ulang, jadi sekaligus berfungsi sebagai idempotency key |
| `webhook-timestamp` | Detik Unix, seperti `1792300000` | Kapan percobaan ini ditandatangani. Baru di setiap percobaan ulang |
| `webhook-signature` | `v1,` ditambah base64 | Satu atau lebih nilai yang dipisahkan spasi. Dua saat secret sedang dirotasi |

`Apa yang ditandatangani`:

```text
webhook-id:        dlv_01k7v2d4n8r2p6s0w3y5b7c9e1
webhook-timestamp: 1792300000
raw body:          {"id":"dlv_01k7...","type":"endpoint.test",...}

signed content = webhook-id + "." + webhook-timestamp + "." + raw body
key            = base64-decode(secret without "whsec_")
signature      = base64(HMAC-SHA256(key, signed content))
header         = "v1," + signature
```

Untuk memverifikasi pengiriman, dengan urutan ini:

1. Baca body mentah sebagai byte atau string persis. Jangan di-parse dulu.
2. Baca ketiga header. Jika ada yang hilang, tolak.
3. Pastikan timestamp berupa bilangan bulat dalam 5 menit dari jam Anda, ke arah mana pun. Jika tidak, tolak.
4. Hapus `whsec_` dari secret Anda dan decode base64 sisanya. 32 byte itulah key HMAC-nya, bukan string itu sendiri.
5. Hitung HMAC-SHA256 dari `{webhook-id}.{webhook-timestamp}.{raw body}` dan encode base64 dengan alfabet standar dan padding.
6. Pisahkan `webhook-signature` berdasarkan spasi. Untuk setiap entri, pisahkan di koma pertama; lewati apa pun yang versinya bukan `v1`.
7. Bandingkan setiap nilai `v1` dengan milik Anda secara constant-time. Jika ada yang cocok, pengiriman itu asli.
8. Baru sekarang parse JSON dan tindak lanjuti.

## Test vector

Sebelum men-deploy handler, buktikan bahwa handler menghitung nilai yang benar. Input ini adalah body pengiriman `endpoint.test` sungguhan yang ditandatangani dengan secret karangan. Kode Anda harus menghasilkan persis tanda tangan di baris terakhir. Setiap panduan bahasa menyertakan unit test yang memakainya.

| Input | Nilai |
| --- | --- |
| Secret | `whsec_Fg8c6hm1Gt41v5RSPKm72LggDx8S1mlWkbBYpHPAnpw=` |
| `webhook-id` | `dlv_01k7v2d4n8r2p6s0w3y5b7c9e1` |
| `webhook-timestamp` | `1792300000` |
| Body mentah | Byte persis di script di bawah, tanpa spasi atau baris baru |
| `webhook-signature` yang diharapkan | `v1,Duq+Iuh0xbRZyS+C/TO5W1hcGLGwg+ifBiWiYkq15qA=` |

Anda bisa memeriksanya tanpa menulis kode. Ini berjalan di terminal macOS atau Linux dan mencetak bagian base64 dari tanda tangan:

`Terminal`:

```bash
SECRET='whsec_Fg8c6hm1Gt41v5RSPKm72LggDx8S1mlWkbBYpHPAnpw='
ID='dlv_01k7v2d4n8r2p6s0w3y5b7c9e1'
TS='1792300000'
BODY='{"id":"dlv_01k7v2d4n8r2p6s0w3y5b7c9e1","type":"endpoint.test","timestamp":"2026-10-17T12:00:00.000Z","api_version":"2026-10-17","data":{"endpoint_id":"whk_01k7v2e1b3d5f7h9k1m3n5q7s9","message":"This is a test event from Ikiform."}}'

KEY=$(printf '%s' "${SECRET#whsec_}" | base64 -d | xxd -p -c 256)
printf '%s.%s.%s' "$ID" "$TS" "$BODY" \
  | openssl dgst -sha256 -mac HMAC -macopt "hexkey:$KEY" -binary \
  | base64
```

Dua kasus lagi layak diuji masing-masing, karena keduanya adalah kegagalan nyata yang paling umum:

| Perubahan | Tanda tangan | Haruskah handler Anda menerimanya? |
| --- | --- | --- |
| Body yang sama diformat rapi dengan indentasi dua spasi | `v1,se0TDhknrG0hYMvCId83HnpZfJcELG3ozxFJktRUHqY=` | Tidak. JSON sama, byte berbeda |
| `webhook-timestamp` satu detik kemudian | `v1,oBfWyLDPfrwVSo40e6b7B7+LmKkZX7OqT88SsrpY6ks=` | Tidak. Timestamp ikut ditandatangani |
| Ditandatangani dengan secret kedua saat rotasi | `v1,gfHP3s7ANdIpnM7gewtWtSvTrHgmsbEFgCaeRRR0Eak=` | Ya, jika header juga punya nilai yang cocok dengan secret Anda |

Saat rotasi secret, header membawa kedua nilai. Handler Anda, yang memegang salah satu secret, harus menerima ini:

`Dua tanda tangan`:

```http
webhook-id: dlv_01k7v2d4n8r2p6s0w3y5b7c9e1
webhook-timestamp: 1792300000
webhook-signature: v1,Duq+Iuh0xbRZyS+C/TO5W1hcGLGwg+ifBiWiYkq15qA= v1,gfHP3s7ANdIpnM7gewtWtSvTrHgmsbEFgCaeRRR0Eak=
```

> Timestamp di vector bersifat tetap, jadi verifier sungguhan menolaknya karena terlalu lama. Test harus memberikan waktu saat ini yang tetap, seperti yang dilakukan panduan bahasa. [Signature verifier](https://ikiform.com/id/docs/webhooks.md#verifier) di halaman webhook memeriksa secret, header, dan body apa pun di browser.

## Cara memverifikasi

| Pendekatan | Unggul dalam | Kekurangan |
| --- | --- | --- |
| `verifyWebhook` dari `@ikiform/sdk/webhooks` | Satu panggilan yang memeriksa semuanya dan mengembalikan event bertipe. Berjalan di Node, Workers, Deno, dan Bun | Hanya JavaScript dan TypeScript |
| Library Standard Webhooks | Kode terawat untuk Python, Go, PHP, Ruby, JavaScript, Rust, Java, C#, dan Elixir | Satu dependency lagi. Periksa cara masing-masing menerima header; sebagian case-sensitive |
| Kode HMAC Anda sendiri | Tanpa dependency, sekitar 20 baris, dan Anda bisa menguji setiap cabang | Mudah keliru secara halus: perbandingan string, byte key yang salah, atau body yang sudah di-parse |
| Path rahasia atau token query sebagai gantinya | Sederhana | Bocor lewat log, proxy, dan error tracker, tidak membuktikan apa pun tentang body, dan tidak bisa kedaluwarsa. Bukan pengganti |

Ikiform menandatangani setiap pengiriman, dan tanda tangan adalah cara yang didukung untuk mengautentikasinya. Dokumentasinya tidak memublikasikan daftar tetap alamat IP pengirim, jadi allowlist IP bukan pemeriksaan yang bisa Anda andalkan.

## Verifikasi di framework Anda

Untuk stack JavaScript, SDK yang melakukan pekerjaannya. Yang berbeda antar framework adalah di mana route berada dan cara mendapatkan body sebelum ada yang mem-parsing-nya. Pilih milik Anda:

**Next.js**

Route handler App Router. `request.text()` adalah body mentah; memanggil `request.json()` lebih dulu akan menghilangkannya. [Panduan server sendiri](https://ikiform.com/blog/form-submissions-to-your-own-server.md) menambahkan dedupe dan `after()`.

`app/api/webhooks/ikiform/route.ts`:

```ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";

export async function POST(request: Request) {
  const payload = await request.text();
  try {
    const event = await verifyWebhook({
      payload,
      headers: request.headers,
      secret: process.env.IKIFORM_WEBHOOK_SECRET!,
    });
    console.info(event.type, request.headers.get("webhook-id"));
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
}
```

**Astro**

Endpoint dengan ekspor `POST`. Endpoint ini butuh rendering on demand, jadi pertahankan `prerender = false` dan sebuah adapter. `getSecret` dari `astro:env/server` membaca `IKIFORM_WEBHOOK_SECRET` saat runtime dan hanya di server; `import.meta.env` akan menanamnya ke dalam build.

`src/pages/api/webhooks/ikiform.ts`:

```ts
import type { APIRoute } from "astro";
import { getSecret } from "astro:env/server";
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";

export const prerender = false;

export const POST: APIRoute = async ({ request }) => {
  const payload = await request.text();
  try {
    const event = await verifyWebhook({
      payload,
      headers: request.headers,
      secret: getSecret("IKIFORM_WEBHOOK_SECRET") ?? "",
    });
    console.info(event.type, event.id);
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
};
```

**TanStack Start**

Server route: route berbasis file yang sama, dengan function `server.handlers.POST` yang menerima `Request` standar.

`src/routes/api/webhooks/ikiform.ts`:

```ts
import { createFileRoute } from "@tanstack/react-router";
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";

export const Route = createFileRoute("/api/webhooks/ikiform")({
  server: {
    handlers: {
      POST: async ({ request }) => {
        const payload = await request.text();
        try {
          const event = await verifyWebhook({
            payload,
            headers: request.headers,
            secret: process.env.IKIFORM_WEBHOOK_SECRET!,
          });
          console.info(event.type, event.id);
          return new Response(null, { status: 204 });
        } catch (error) {
          if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
          throw error;
        }
      },
    },
  },
});
```

**React Router**

Resource route: modul dengan `action` dan tanpa default export, didaftarkan di `routes.ts`. Route ini menjawab Ikiform langsung alih-alih merender halaman.

`app/routes.ts`:

```ts
import { type RouteConfig, index, route } from "@react-router/dev/routes";

export default [
  index("routes/home.tsx"),
  route("webhooks/ikiform", "routes/webhooks.ikiform.ts"),
] satisfies RouteConfig;
```

`app/routes/webhooks.ikiform.ts`:

```ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";
import type { Route } from "./+types/webhooks.ikiform";

export async function action({ request }: Route.ActionArgs) {
  if (request.method !== "POST") return new Response(null, { status: 405 });
  const payload = await request.text();
  try {
    const event = await verifyWebhook({
      payload,
      headers: request.headers,
      secret: process.env.IKIFORM_WEBHOOK_SECRET!,
    });
    console.info(event.type, event.id);
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
}
```

**SvelteKit**

Endpoint `+server.ts`. Pemeriksaan CSRF bawaan SvelteKit hanya memblokir content type formulir lintas situs, dan Ikiform mengirim `application/json`, jadi tidak perlu mengubah config. Deklarasikan `IKIFORM_WEBHOOK_SECRET` di `src/env.ts` dengan `defineEnvVars` agar `$app/env/private` bisa membacanya.

`src/routes/webhooks/ikiform/+server.ts`:

```ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";
import { IKIFORM_WEBHOOK_SECRET } from "$app/env/private";
import type { RequestHandler } from "./$types";

export const POST: RequestHandler = async ({ request }) => {
  const payload = await request.text();
  try {
    const event = await verifyWebhook({ payload, headers: request.headers, secret: IKIFORM_WEBHOOK_SECRET });
    console.info(event.type, event.id);
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
};
```

**Nuxt**

Server route Nitro. `readRawBody` mengembalikan body sebagai string sebelum ada yang mem-parsing-nya. Atur `NUXT_IKIFORM_WEBHOOK_SECRET` dan deklarasikan `ikiformWebhookSecret` di bawah `runtimeConfig` di `nuxt.config.ts`.

`server/api/webhooks/ikiform.post.ts`:

```ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";

export default defineEventHandler(async (event) => {
  const payload = (await readRawBody(event, "utf8")) ?? "";
  const { ikiformWebhookSecret } = useRuntimeConfig(event);
  try {
    const delivery = await verifyWebhook({
      payload,
      headers: getRequestHeaders(event),
      secret: ikiformWebhookSecret,
    });
    console.info(delivery.type, delivery.id);
    setResponseStatus(event, 204);
    return null;
  } catch (error) {
    if (error instanceof WebhookVerificationError) {
      throw createError({ status: 400, message: error.reason });
    }
    throw error;
  }
});
```

Setiap handler menjawab `204` saat sukses dan `400` beserta alasan kegagalan jika tidak. Lakukan pekerjaan lambat setelah menjawab; Ikiform menunggu 15 detik sebelum menganggap percobaan gagal.

## Mengapa body mentah penting

Tanda tangan mencakup byte, bukan makna. `{"a":1}` dan `{ "a": 1 }` adalah JSON yang sama dengan byte berbeda, jadi body yang sudah di-parse lalu diserialisasi ulang hampir tidak pernah cocok. Hal yang sama berlaku untuk apa pun yang menyentuh body saat masuk: middleware yang mem-parsing JSON, proxy yang meng-encode ulang, atau framework yang memberi Anda objek alih-alih string.

Baca body sekali, sebagai byte atau string, verifikasi, lalu parse string yang sama itu. Setiap panduan bahasa menunjukkan di mana body mentah berada di framework-nya, dan [panduan server sendiri](https://ikiform.com/blog/form-submissions-to-your-own-server.md) punya tabel untuk framework Node dan Python yang umum.

## Bandingkan secara constant-time

Perbandingan string biasa berhenti di karakter pertama yang berbeda. Secara teori, penyerang yang bisa mengukur waktu ribuan request akan tahu berapa banyak karakter awal tebakannya yang benar dan menyusun tanda tangan valid satu karakter demi satu karakter. Perbandingan constant-time memakan waktu yang sama berapa pun bagian yang cocok. Setiap bahasa punya:

| Bahasa | Perbandingan constant-time |
| --- | --- |
| Node.js | `crypto.timingSafeEqual(a, b)`, yang melempar error jika panjangnya berbeda, jadi periksa panjang lebih dulu |
| Python | `hmac.compare_digest(a, b)` |
| Go | `hmac.Equal(a, b)` |
| PHP | `hash_equals($known, $user)` |
| Ruby | `OpenSSL.secure_compare(a, b)`, atau `ActiveSupport::SecurityUtils.secure_compare` di Rails |

## Timestamp dan replay

Request valid yang tertangkap sekali akan tetap valid selamanya kecuali ada yang membuatnya kedaluwarsa. Timestamp yang ditandatangani itulah yang melakukannya: tolak pengiriman apa pun yang selisihnya lebih dari 5 menit dari jam Anda, ke arah mana pun. Karena Ikiform menandatangani ulang setiap percobaan dengan timestamp baru, percobaan ulang tiga hari kemudian tetap lolos; hanya request lama yang disalin yang gagal.

Dalam 5 menit itu, replay masih mungkin, begitu pula duplikat yang asli: Ikiform mencoba ulang saat tidak mendapat `2xx` tepat waktu, meski handler Anda sudah selesai. Simpan `webhook-id` dengan unique constraint dan lewati id yang sudah Anda tangani. Itu menutup keduanya.

> Jaga jam server tetap sinkron. Jam yang bergeser lebih dari 5 menit menolak setiap pengiriman dengan `timestamp_too_old` atau `timestamp_too_new`. Platform terkelola biasanya menjaga jam tetap sinkron; di VM Anda sendiri, jalankan NTP.

## Merotasi secret

Rotasi secret saat orang yang mengetahuinya keluar, saat secret mungkin tercatat di log atau ter-commit, atau secara terjadwal. **Roll signing secret** di halaman endpoint membuat secret baru, dan selama 24 jam setiap pengiriman ditandatangani dengan keduanya:

Diagram: Isi webhook-signature sebelum, selama, dan setelah rotasi secret

1. Sebelum rotasi, setiap pengiriman membawa satu tanda tangan, dibuat dengan secret lama.
2. Anda memilih Roll signing secret dan menyalin secret baru.
3. Selama 24 jam berikutnya, setiap pengiriman membawa dua tanda tangan, jadi server yang memegang salah satu secret akan menerimanya. Deploy secret baru dalam jendela waktu ini.
4. Setelah 24 jam, pengiriman hanya membawa tanda tangan baru, dan server yang masih memegang secret lama menolaknya.

Handler Anda tidak butuh kode khusus untuk ini, selama handler memeriksa setiap entri v1, bukan hanya yang pertama.

Jika secret lama masih ter-deploy setelah 24 jam, setiap pengiriman gagal. Ikiform mencoba ulang masing-masing 8 kali selama sekitar tiga hari, jadi setelah Anda men-deploy secret yang benar, gunakan **Retry** di log pengiriman untuk yang sudah menyerah.

## Setelah pemeriksaan lolos

Tanda tangan yang valid membuktikan request berasal dari Ikiform dan tidak diubah. Itu tidak membuktikan kontennya aman atau baru. Tiga kebiasaan menyelesaikan pekerjaan ini:

- **Lewati pengulangan.** Simpan `webhook-id`, atau `id` di payload, yang nilainya sama, dengan unique constraint. Percobaan ulang dari pengiriman yang sudah Anda tangani menjadi no-op.
- **Jawab dulu, kerjakan belakangan.** Kembalikan `2xx` begitu event disimpan atau dimasukkan ke antrean. Ikiform menganggap apa pun yang lebih lambat dari 15 detik sebagai kegagalan dan mencoba ulang, yang memunculkan kembali masalah duplikat.
- **Perlakukan field sebagai tidak tepercaya.** `data.fields` berisi apa yang diketik pengunjung di formulir publik. Escape sebelum merendernya di HTML, pesan chat, atau email, dan jangan pernah meneruskannya ke shell atau query tanpa parameter.

### Menguji sebelum live

Ikiform hanya mengirim ke URL `https:` publik di port 443, jadi `localhost` tidak bisa menerima webhook. Jalankan tunnel seperti `cloudflared tunnel --url http://localhost:3000` dan tambahkan URL yang dicetaknya sebagai endpoint kedua selama Anda bekerja. **Send test** di halaman endpoint mengirim event `endpoint.test` sekali, tanpa percobaan ulang, ditandatangani persis seperti pengiriman sungguhan. Hapus endpoint uji setelah selesai; endpoint itu punya secret sendiri, jadi tidak ada yang lain berubah.

Jika pengiriman gagal dan Anda tidak tahu sebabnya, salin header dan body-nya dari log Anda ke [signature verifier](https://ikiform.com/id/docs/webhooks.md#verifier). Alat itu menjalankan pemeriksaan yang sama di browser dan memberi tahu langkah mana yang gagal.

## Lima cara verifikasi bisa salah

| Kesalahan | Gejala | Solusi |
| --- | --- | --- |
| Memakai seluruh string `whsec_...` sebagai key | `no_matching_signature` di setiap pengiriman | Hapus `whsec_`, lalu decode base64. Key-nya 32 byte |
| Memverifikasi body yang sudah di-parse | `no_matching_signature`, terkadang hanya untuk sebagian payload | Verifikasi body mentah; parse sesudahnya |
| Hanya memeriksa tanda tangan pertama | Kegagalan yang dimulai tepat setelah rotasi | Loop setiap entri `v1` yang dipisahkan spasi |
| Heksadesimal alih-alih base64 | `no_matching_signature` | Standard Webhooks memakai base64 standar dengan padding, bukan hex atau base64url |
| Mencari header dengan huruf besar-kecil yang persis | `missing_headers` di balik beberapa proxy | Nama header tidak peka huruf besar-kecil. Ikiform mengirimnya dalam huruf kecil; cari tanpa memedulikan huruf besar-kecil |

Tidak ada integrasi sekali klik. Setiap alat dijangkau lewat [webhook](https://ikiform.com/id/docs/webhooks.md) bertanda tangan, langsung atau melalui alat otomasi seperti [Zapier](https://ikiform.com/blog/form-submissions-to-zapier.md) atau [n8n](https://ikiform.com/blog/form-submissions-to-n8n.md). Satu workspace bisa punya 3 endpoint, masing-masing dengan secret sendiri. Payload lengkap, jadwal percobaan ulang, dan log pengiriman ada di [Webhooks](https://ikiform.com/id/docs/webhooks.md).

## Pilih situasi Anda

Setiap halaman bahasa berisi handler yang idiomatis, opsi library, dan unit test yang menjalankan test vector dari halaman ini.

- [Node.js](https://ikiform.com/id/guides/webhook-signature-verification/nodejs.md): Verifikasi tanda tangan webhook Ikiform di Node.js: body mentah di Express dan Fastify, SDK, paket standardwebhooks, dan versi node:crypto yang sudah diuji.
- [Python](https://ikiform.com/id/guides/webhook-signature-verification/python.md): Verifikasi tanda tangan webhook Ikiform di Flask, FastAPI, dan Django, dengan paket standardwebhooks atau standard library, ditambah test pytest.
- [Go net/http](https://ikiform.com/id/guides/webhook-signature-verification/go.md): Verifikasi tanda tangan webhook Ikiform di Go dengan net/http: baca body sekali, periksa dengan crypto/hmac, uji, atau gunakan library Standard Webhooks.
- [PHP](https://ikiform.com/id/guides/webhook-signature-verification/php.md): Verifikasi tanda tangan webhook Ikiform di PHP biasa dengan hash_hmac dan hash_equals, dan di Laravel dengan library Standard Webhooks dan pengecualian CSRF.
- [Ruby](https://ikiform.com/id/guides/webhook-signature-verification/ruby.md): Verifikasi tanda tangan webhook Ikiform di Sinatra dengan OpenSSL dan test Minitest, dan di Rails dengan gem standardwebhooks dan request.raw_post.

## Pastikan berfungsi

Lakukan ini di endpoint staging dulu, lalu sekali lagi di produksi. Ikiform tidak punya paket gratis. Pengujian gratis: workspace tanpa paket menerima 50 kiriman uji per bulan dari `localhost`. Untuk live, pilih Pro mulai $6/bln untuk 500 kiriman, atau bayar sekali: 10.000 kiriman seharga $49 atau 50.000 kiriman seharga $149, dengan kredit yang tidak pernah kedaluwarsa. Lihat [harga](https://ikiform.com/id/pricing).

1. Jalankan test vector lewat kode Anda: kode harus menghasilkan tanda tangan yang diharapkan, menerima header dengan dua tanda tangan, dan menolak body yang diformat rapi.
2. Di dashboard, buka **Webhooks**, tambahkan URL `https:` publik endpoint Anda, dan salin secret `whsec_` ke environment server Anda.
3. Pilih **Send test** di halaman endpoint. Log pengiriman menampilkan `204` Anda dan event sampai ke log Anda.
4. Ubah satu karakter secret di server Anda dan kirim uji lagi. Log menampilkan `400` dengan `no_matching_signature`.
5. Kirim formulir sungguhan dan pastikan pengiriman `submission.created` berhasil.

## Pemecahan masalah

| Masalah | Yang harus dilakukan |
| --- | --- |
| `400 no_matching_signature` | Secret salah, body sudah di-parse, atau key yang tidak di-decode base64. Jalankan test vector lewat jalur kode yang sama untuk melihat yang mana. |
| `400 timestamp_too_old` atau `timestamp_too_new` | Jam server meleset, atau Anda memutar ulang request tersimpan secara manual. Sinkronkan jam, dan gunakan **Retry** di log pengiriman, yang menandatangani ulang. |
| `400 missing_headers` | Proxy atau framework membuang header `webhook-*`, atau request tidak berasal dari Ikiform. Catat setiap nama header yang dilihat handler Anda. |
| Berhasil untuk event uji, gagal untuk kiriman sungguhan | Payload sungguhan berisi karakter non-ASCII dan teks yang lebih panjang. Kode Anda men-decode atau meng-encode ulang body sebelum verifikasi. Verifikasi byte yang persis. |
| Setiap pengiriman gagal tepat setelah merotasi secret | Handler hanya memeriksa tanda tangan pertama, atau 24 jam sudah lewat dengan secret lama masih ter-deploy. Periksa setiap entri `v1` dan deploy secret baru. |
| Log pengiriman menampilkan timeout, padahal handler Anda berjalan | Ikiform menunggu 15 detik. Jawab dulu dan lakukan pekerjaan lambat sesudahnya, dan lewati nilai `webhook-id` yang berulang. |

## Pertanyaan

### Bagaimana cara memverifikasi tanda tangan webhook?

Hitung ulang HMAC-SHA256 dari id webhook, timestamp, dan body mentah yang digabung dengan titik, memakai bagian hasil decode base64 dari secret whsec_ Anda sebagai key. Encode hasilnya dengan base64 dan bandingkan secara constant-time dengan setiap nilai v1 di header webhook-signature.

### Mengapa tanda tangan webhook saya tidak pernah cocok?

Hampir selalu salah satu dari tiga hal: body di-parse dan diserialisasi ulang sebelum verifikasi, seluruh string whsec_ dipakai sebagai key alih-alih byte hasil decode base64-nya, atau secret-nya milik endpoint lain.

### Apa itu Standard Webhooks?

Spesifikasi terbuka untuk menandatangani webhook dengan HMAC-SHA256 dan header webhook-id, webhook-timestamp, dan webhook-signature. Ikiform mengikutinya, jadi library Standard Webhooks apa pun bisa memverifikasi pengiriman Ikiform.

### Mengapa menolak timestamp lama?

Jika tidak, request yang tertangkap akan tetap valid selamanya. Menolak apa pun yang selisihnya lebih dari 5 menit dari jam Anda menghentikan replay request lama, dan Ikiform menandatangani setiap percobaan ulang dengan timestamp baru, jadi percobaan ulang yang asli tetap lolos.

### Apa yang terjadi pada pengiriman saat saya merotasi secret?

Selama 24 jam setiap pengiriman ditandatangani dengan secret lama maupun baru, jadi Anda bisa men-deploy yang baru tanpa kehilangan event. Setelah itu hanya secret baru yang berfungsi.

### Haruskah handler saya mengembalikan 401 atau 400 untuk tanda tangan yang salah?

Keduanya boleh bagi Ikiform; status apa pun selain 2xx dianggap kegagalan dan dicoba ulang. Jangan kembalikan 410, yang langsung menonaktifkan endpoint.

## Panduan lainnya

- [Node.js](https://ikiform.com/id/guides/webhook-signature-verification/nodejs.md): Verifikasi tanda tangan webhook Ikiform di Node.js: body mentah di Express dan Fastify, SDK, paket standardwebhooks, dan versi node:crypto yang sudah diuji.
- [Python](https://ikiform.com/id/guides/webhook-signature-verification/python.md): Verifikasi tanda tangan webhook Ikiform di Flask, FastAPI, dan Django, dengan paket standardwebhooks atau standard library, ditambah test pytest.
- [Go net/http](https://ikiform.com/id/guides/webhook-signature-verification/go.md): Verifikasi tanda tangan webhook Ikiform di Go dengan net/http: baca body sekali, periksa dengan crypto/hmac, uji, atau gunakan library Standard Webhooks.
- [PHP](https://ikiform.com/id/guides/webhook-signature-verification/php.md): Verifikasi tanda tangan webhook Ikiform di PHP biasa dengan hash_hmac dan hash_equals, dan di Laravel dengan library Standard Webhooks dan pengecualian CSRF.
- [Ruby](https://ikiform.com/id/guides/webhook-signature-verification/ruby.md): Verifikasi tanda tangan webhook Ikiform di Sinatra dengan OpenSSL dan test Minitest, dan di Rails dengan gem standardwebhooks dan request.raw_post.
- [Daftar tunggu untuk SaaS baru](https://ikiform.com/id/guides/waitlist.md): Bangun daftar tunggu pra-peluncuran di landing page Anda sendiri: formulir, perlindungan spam, duplikat, email konfirmasi, dan ekspor rapi di hari peluncuran.
- [Formulir kontak di situs statis](https://ikiform.com/id/guides/contact-form-static-site.md): Pasang formulir kontak yang berfungsi di situs statis tanpa server: ke mana POST dikirim, spam, balasan lewat email, dan kode untuk setiap host dan framework.
- [Kirim formulir ke email Anda](https://ikiform.com/id/guides/send-form-to-email.md): Terima setiap kiriman formulir di inbox tanpa mail server: mengapa mailto: dan mail() PHP gagal, dan setup yang menyaring spam serta memungkinkan Anda membalas.
