---
title: POST /webhooks
source: https://se-hari.com/docs/api/api-webhooks-create
updated: 2026-08-15T18:52:42.21857+00:00
---

> Daftarkan endpoint webhook. POST /webhooks — butuh scope webhooks:write.

Mendaftarkan URL yang akan menerima pemberitahuan setiap kali salah satu event yang dipilih terjadi. Ini yang membuat integrasi terasa hidup: tanpa webhook, klien harus melakukan polling dan tetap saja terlambat. URL wajib https di port 443, dan nama hostnya tidak boleh mengarah ke alamat jaringan privat — pemeriksaan itu diulang sebelum setiap pengiriman, bukan hanya saat pendaftaran. Respons memuat `secret` yang dipakai memverifikasi tanda tangan; simpan sekarang, karena ia hanya muncul lagi lewat endpoint detail. Setiap pengiriman membawa header `X-Sehari-Signature` berformat `t=<unix>,v1=<hex>` di mana hex adalah HMAC-SHA256 dari string `"{t}.{body mentah}"`. Verifikasi WAJIB memakai body mentah (bukan hasil parse lalu stringify ulang), perbandingan constant-time, dan penolakan timestamp yang lebih tua dari 5 menit. Satu URL hanya boleh didaftarkan sekali per akun.
## Endpoint
```bash
POST https://se-hari.com/api/v1/webhooks
```
Butuh scope `webhooks:write`. Scope ditetapkan saat key dibuat dan **tidak bisa ditambahkan belakangan** — kalau key Anda kurang izin, buat key baru lalu cabut yang lama. Kekakuan ini disengaja: scope yang bisa dinaikkan diam-diam berarti key yang bocor hari ini bisa menjadi lebih berbahaya besok. Beri setiap integrasi key sendiri dengan scope sesempit mungkin, supaya mencabut satu integrasi tidak pernah berarti mematikan yang lain.
Kirim header `Idempotency-Key` berisi nilai unik milik Anda sendiri; UUID sudah cukup. Kalau request diulang dengan key yang sama — karena timeout jaringan, retry otomatis n8n, atau tombol yang terklik dua kali — kami mengembalikan hasil yang pertama alih-alih mengerjakannya lagi. Key yang sama dengan body berbeda ditolak `idempotency_key_reused`, karena itu hampir selalu berarti bug di sisi pemanggil, bukan permintaan yang sah.
Operasi ini tidak memotong kredit saat dipanggil. Kalau ia mengantre pekerjaan notulen, penagihan terjadi **setelah** pemrosesan selesai dan dihitung dari durasi asli rekaman — bukan dari durasi yang Anda perkirakan. Perbedaan ini penting saat membaca saldo di langkah berikutnya sebuah workflow.
## Body request

| Field | Tipe | Wajib | Keterangan |
|---|---|---|---|
| `url` | `string` | ya | maks 1000 karakter, uri |
| `event_types` | `array<note.completed|note.failed|meeting.created|meeting.ended|recording.ready|credit.low|credit.topup.succeeded|webhook.test>` | ya | — |
| `description` | `string` | tidak | maks 200 karakter |

## Contoh
```bash
curl -X POST "https://se-hari.com/api/v1/webhooks" \
  -H "Authorization: Bearer $SEHARI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://n8n.perusahaan.com/webhook/sehari","event_types":["note.completed","recording.ready"],"description":"Kirim notulen ke Slack"}'
```
```javascript
const res = await fetch('https://se-hari.com/api/v1/webhooks', {
	method: 'POST',
		headers: {
				'Authorization': `Bearer ${process.env.SEHARI_API_KEY}`,
				'Content-Type': 'application/json',
			},
	body: JSON.stringify({
	 "url": "https://n8n.perusahaan.com/webhook/sehari",
	 "event_types": [
	  "note.completed",
	  "recording.ready"
	 ],
	 "description": "Kirim notulen ke Slack"
	})
});

if (!res.ok) {
	const { error } = await res.json();
	// error.code stabil dan bisa dicabang; error.message untuk manusia.
	throw new Error(`${error.code}: ${error.message}`);
}

const data = await res.json();
```
```python
import os, requests

res = requests.post(
    "https://se-hari.com/api/v1/webhooks",
    headers={"Authorization": f"Bearer {os.environ['SEHARI_API_KEY']}"},
    json={"url":"https://n8n.perusahaan.com/webhook/sehari","event_types":["note.completed","recording.ready"],"description":"Kirim notulen ke Slack"},
    timeout=30,
)

if not res.ok:
    err = res.json()["error"]
    raise RuntimeError(f"{err['code']}: {err['message']}")

data = res.json()
```
## Field respons

| Field | Tipe | Keterangan |
|---|---|---|
| `id` | `string` | — |
| `url` | `string` | — |
| `description` | `string` \| `null` | — |
| `event_types` | `array<string>` | — |
| `is_active` | `boolean` | — |
| `consecutive_failures` | `integer` | — |
| `disabled_at` | `string` \| `null` | — |
| `created_by` | `string` | — |
| `created_at` | `string` | — |
| `secret` | `string` | — |

## Kalau gagal
Setiap kegagalan memakai amplop yang sama, dan `code` di dalamnya stabil — cabangkan logika Anda ke sana, jangan ke `message` yang teksnya bisa diperbaiki sewaktu-waktu.
```json
{
  "error": {
    "code": "invalid_request",
    "message": "Penjelasan untuk manusia.",
    "docs_url": "https://se-hari.com/docs/error/invalid-request",
    "request_id": "req_a1b2c3d4e5f6",
    "details": {}
  }
}
```
| Status | Kode | Artinya di endpoint ini |
|---|---|---|
| 400 | [`invalid_request`](/docs/error/invalid-request) | Body tidak lolos validasi. Field yang bermasalah disebut di `details`. |
| 401 | [`invalid_api_key`](/docs/error/invalid-api-key) | Key salah, sudah dicabut, atau kedaluwarsa. |
| 403 | [`insufficient_scope`](/docs/error/insufficient-scope) | Key valid, tapi tidak punya scope yang dibutuhkan endpoint ini. |
| 409 | [`conflict`](/docs/error/conflict) | Bentrok dengan kondisi data sekarang. |
| 422 | [`unprocessable_entity`](/docs/error/unprocessable-entity) | Bentuk request benar, tapi isinya tidak bisa diproses. |
| 429 | [`rate_limited`](/docs/error/rate-limited) | Terlalu banyak request. Hormati header `Retry-After`. |
| 500 | [`internal_error`](/docs/error/internal-error) | Kesalahan di sisi kami. Sertakan `request_id` saat melapor. |
Simpan `request_id` dari setiap respons gagal. Itu satu-satunya cara kami menemukan kembali request Anda di log ketika Anda melapor, dan ia juga ada di header `X-Request-Id` pada respons yang berhasil.
## Alur khas

Webhook membalik arah komunikasi: alih-alih Anda menanyakan status berulang kali, Se-Hari yang memanggil endpoint Anda begitu ada kabar. Ini satu-satunya cara yang masuk akal untuk mengetahui notulen selesai, karena pemrosesannya bisa memakan beberapa menit.

1. `POST /webhooks` dengan URL https publik Anda dan daftar event — simpan `secret` dari respons
2. Verifikasi header `X-Sehari-Signature` di setiap kiriman yang masuk
3. Jawab 2xx secepatnya; kerjakan proses beratnya setelah membalas
4. `GET /webhooks/{id}/deliveries` kalau ada yang tampak tidak sampai

`secret` hanya ditampilkan sekali, saat endpoint dibuat. Kalau hilang, pakai `POST /webhooks/{id}/rotate-secret` untuk mendapatkan yang baru — dan ingat bahwa kiriman yang sedang dalam antrean retry akan ditandatangani dengan secret baru itu.

## Yang sering keliru

- **Key disalin sebagian.** API key Se-Hari panjangnya tetap: `sh_live_` diikuti 43 karakter. Kalau panjangnya berbeda, yang salah adalah proses penyalinannya, bukan key-nya — dan pesan 401 tidak akan mengatakan itu.
- **Scope kurang, bukan key salah.** 403 di sini berarti key-nya dikenali. Periksa scope-nya lewat `GET /me`, lalu buat key baru kalau memang kurang; scope tidak bisa ditambahkan ke key yang sudah jadi.
- **409 biasanya berarti pekerjaannya sudah ada.** Cek `details` pada respons: di sana ada id data yang sudah lebih dulu menempati posisi itu, dan biasanya itulah yang sebenarnya Anda cari.
- **Hormati `Retry-After`.** Mencoba lagi lebih cepat dari yang disebutkan hanya memperpanjang masa tunggu. Kalau Anda sering menyentuhnya, kemungkinan besar Anda sedang polling sesuatu yang seharusnya ditangani webhook.

## Selanjutnya
- [Autentikasi dan API key](/docs/autentikasi) — cara membuat key dan memilih scope
- [Kode error](/docs/error) — arti setiap kode dan cara memperbaikinya
- [Idempotency](/docs/idempotency) — kenapa retry aman kalau dilakukan dengan benar
- [Spesifikasi OpenAPI](https://se-hari.com/api/v1/openapi.json) — kontrak mesin, cocok untuk men-generate klien