Langsung ke konten
↑↓ navigasi↵ bukaEsc tutup

Panduan

Webhook

TheComNet mengirim (POST) event JSON bertanda tangan ke endpoint HTTPS Anda saat pesanan, retur, stok, atau chat berubah.

Penyiapan

  1. Di TheComNet buka Pengaturan → Integrasi lalu tambahkan URL endpoint webhook (maksimal 10 per tenant).
  2. Pilih event yang ingin diikuti.
  3. Salin signing secret (whsec_…). Secret bisa ditampilkan lagi atau dirotasi nanti; setelah rotasi, hanya secret baru yang menandatangani.
  4. Tekan Kirim tes untuk mengirim event ping lalu periksa log pengiriman.

Event

EventDikirim saat
order.createdPesanan baru masuk dari toko yang terhubung. previousStatus bernilai null.
order.status_changedPesanan berpindah ke status ternormalisasi lain (paid, shipped, delivered, cancelled, …).
aftersale.createdPembeli membuka permintaan pembatalan atau retur. type dan status adalah nilai milik marketplace.
aftersale.completedRetur dengan barang yang dikirim balik telah selesai.
inventory.stock_changedStok yang bisa dijual (stok fisik dikurangi safety stock, tidak pernah di bawah 0) dari item master berubah.
chat.message_receivedPembeli mengirim pesan chat. text berisi deskripsi singkat untuk pesan non-teks.
pingAnda menekan Kirim tes di konsol. data berisi { "message": "Webhook test from TheComNet" }. Tidak bisa diikuti.

Envelope

Setiap pengiriman memiliki bentuk tingkat atas yang sama:

FieldTipeArti
idstring (uuid)Id event. Event yang sama yang dikirim lagi (retry atau Kirim ulang) tetap memakai id ini; lakukan dedupe dengannya.
typestringTipe event, juga dikirim sebagai x-thecomnet-event.
createdAtstring (date-time)Waktu event disebarkan ke endpoint Anda (UTC).
tenantIdstring (uuid)Tenant pemilik event.
dataobjectPayload khusus event, lihat di bawah.
simulatedbooleanHanya ada pada event simulasi, selalu true.

order.created

POST body
{
  "id": "0192f0d0-aaaa-7bbb-8ccc-ddddeeeeffff",
  "type": "order.created",
  "createdAt": "2026-11-04T07:12:31.204Z",
  "tenantId": "0192f0c0-1111-7aaa-8bbb-0123456789ab",
  "data": {
    "orderId": "0192f0c6-3b4c-7d5e-9f60-718293a4b5c6",
    "storeId": "0192f0c2-0a1b-7c2d-8e3f-4a5b6c7d8e9f",
    "marketplace": "tiktok",
    "externalOrderId": "578123456789012345",
    "status": "pending_payment",
    "previousStatus": null,
    "placedAt": "2026-11-04T07:12:09.000Z"
  }
}

order.status_changed

POST body
{
  "id": "0192f0d0-aaaa-7bbb-8ccc-ddddeeeeffff",
  "type": "order.status_changed",
  "createdAt": "2026-11-04T07:12:31.204Z",
  "tenantId": "0192f0c0-1111-7aaa-8bbb-0123456789ab",
  "data": {
    "orderId": "0192f0c6-3b4c-7d5e-9f60-718293a4b5c6",
    "storeId": "0192f0c2-0a1b-7c2d-8e3f-4a5b6c7d8e9f",
    "marketplace": "tiktok",
    "externalOrderId": "578123456789012345",
    "status": "shipped",
    "previousStatus": "processing",
    "placedAt": "2026-11-04T07:12:09.000Z"
  }
}

aftersale.created

POST body
{
  "id": "0192f0d0-aaaa-7bbb-8ccc-ddddeeeeffff",
  "type": "aftersale.created",
  "createdAt": "2026-11-04T07:12:31.204Z",
  "tenantId": "0192f0c0-1111-7aaa-8bbb-0123456789ab",
  "data": {
    "orderId": "0192f0c6-3b4c-7d5e-9f60-718293a4b5c6",
    "storeId": "0192f0c2-0a1b-7c2d-8e3f-4a5b6c7d8e9f",
    "externalOrderId": "578123456789012345",
    "externalReturnId": "4035987654321098765",
    "type": "RETURN_AND_REFUND",
    "status": "RETURN_OR_REFUND_REQUEST_PENDING",
    "reason": "Item does not match description"
  }
}

aftersale.completed

POST body
{
  "id": "0192f0d0-aaaa-7bbb-8ccc-ddddeeeeffff",
  "type": "aftersale.completed",
  "createdAt": "2026-11-04T07:12:31.204Z",
  "tenantId": "0192f0c0-1111-7aaa-8bbb-0123456789ab",
  "data": {
    "orderId": "0192f0c6-3b4c-7d5e-9f60-718293a4b5c6",
    "storeId": "0192f0c2-0a1b-7c2d-8e3f-4a5b6c7d8e9f",
    "externalReturnId": "4035987654321098765"
  }
}

inventory.stock_changed

POST body
{
  "id": "0192f0d0-aaaa-7bbb-8ccc-ddddeeeeffff",
  "type": "inventory.stock_changed",
  "createdAt": "2026-11-04T07:12:31.204Z",
  "tenantId": "0192f0c0-1111-7aaa-8bbb-0123456789ab",
  "data": {
    "itemId": "0192f0c8-5d6e-7f80-91a2-b3c4d5e6f708",
    "sku": "KAOS-HITAM-L",
    "name": "Kaos Polos Hitam L",
    "previousAvailable": 12,
    "available": 11,
    "reason": "order"
  }
}

chat.message_received

POST body
{
  "id": "0192f0d0-aaaa-7bbb-8ccc-ddddeeeeffff",
  "type": "chat.message_received",
  "createdAt": "2026-11-04T07:12:31.204Z",
  "tenantId": "0192f0c0-1111-7aaa-8bbb-0123456789ab",
  "data": {
    "conversationId": "0192f0c9-6e7f-7091-a2b3-c4d5e6f70819",
    "storeId": "0192f0c2-0a1b-7c2d-8e3f-4a5b6c7d8e9f",
    "externalMessageId": "7301234567890123456",
    "text": "Kak, pesanan saya sudah dikirim belum?"
  }
}

Catatan payload

  • status dan previousStatus pesanan adalah status ternormalisasi: pending_payment, processing, shipped, delivered, completed, cancelled, returned. Status khusus marketplace ada di pesanan (marketplaceStatus); ambil dengan GET /v1/orders/{orderId}.
  • reason pada inventory.stock_changed adalah alasan mutasi stok: adjust, set, order, cancel, receive, issue, transfer_in, transfer_out, stocktake, return.
  • Event hanya membawa id dan field yang berubah. Ambil resource lengkap dari API bila butuh lebih.

Event simulasi

Tekan Simulasikan event di Pengaturan → Integrasi → Webhook (atau panggil POST /v1/developer/sandbox/events dengan session token) untuk mengantrekan event contoh dari tipe apa pun ke setiap endpoint yang berlangganan. Event ini membawa simulated: true dan tanda tangan seperti biasa. Abaikan event seperti ini di produksi. Lihat Sandbox: event simulasi.

Header request

HeaderNilai
content-typeapplication/json
user-agentTheComNet-Webhooks/1.0
x-thecomnet-eventTipe event, misalnya order.created.
x-thecomnet-deliveryId pengiriman (satu per endpoint dan event). Tetap sama di setiap retry dan Kirim ulang.
x-thecomnet-signaturet=<unix seconds>,v1=<hex>

Memverifikasi tanda tangan

  1. Parse x-thecomnet-signature menjadi t (detik Unix) dan v1.
  2. Hitung v1 = hex(HMAC-SHA256(secret, `${t}.${rawBody}`)) atas raw request body persis seperti yang diterima. Secret-nya adalah seluruh string whsec_….
  3. Bandingkan dengan v1 yang diterima secara constant-time.
  4. Tolak request bila t berselisih lebih dari 5 menit dari jam Anda (perlindungan replay). Setiap percobaan ditandatangani dengan t yang baru.
import crypto from 'node:crypto';
import express from 'express';

const TOLERANCE_SECONDS = 5 * 60;

/** True when header `t=<unix>,v1=<hex>` signs rawBody with secret and is fresh. */
export function verifySignature(secret, header, rawBody, now = Math.floor(Date.now() / 1000)) {
  const parts = Object.fromEntries(
    header.split(',').map((kv) => {
      const i = kv.indexOf('=');
      return [kv.slice(0, i).trim(), kv.slice(i + 1).trim()];
    }),
  );
  const { t, v1 } = parts;
  if (!/^\d+$/.test(t ?? '') || !/^[0-9a-f]{64}$/.test(v1 ?? '')) return false;
  if (Math.abs(now - Number(t)) > TOLERANCE_SECONDS) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
  return crypto.timingSafeEqual(expected, Buffer.from(v1, 'hex'));
}

const app = express();
const seen = new Set(); // use a database table in production

// Raw body: the signature covers the exact bytes sent, not re-serialized JSON.
app.post('/webhooks/thecomnet', express.raw({ type: 'application/json' }), (req, res) => {
  const raw = req.body.toString('utf8');
  const ok = verifySignature(process.env.THECOMNET_WEBHOOK_SECRET, req.get('x-thecomnet-signature') ?? '', raw);
  if (!ok) return res.sendStatus(400);

  const event = JSON.parse(raw);
  res.sendStatus(204); // acknowledge within 10 s, process afterwards
  if (seen.has(event.id)) return; // at-least-once: dedupe on the event id
  seen.add(event.id);
  setImmediate(() => handle(event));
});

function handle(event) {
  switch (event.type) {
    case 'order.created':
    case 'order.status_changed':
      console.log(event.data.externalOrderId, event.data.previousStatus, '->', event.data.status);
      break;
    case 'ping':
      console.log('test delivery received');
      break;
  }
}

app.listen(3000);

Test vector

InputNilai
secretwhsec_test
t1700000000
body{"a":1}
signed string1700000000.{"a":1}
v138877139021993b830af32feea6e18a8da83eb2f6e49ee50bd9e4cf4ca4d3789
verifySignature('whsec_test', 't=1700000000,v1=38877139021993b830af32feea6e18a8da83eb2f6e49ee50bd9e4cf4ca4d3789', '{"a":1}', 1700000000);
// => true

Semantik pengiriman

  • Balas dengan 2xx apa pun dalam 10 detik. Selain itu (status lain, timeout, error koneksi) dihitung sebagai percobaan gagal. Akui dulu, lalu proses secara asinkron.
  • Redirect tidak diikuti; 3xx dihitung gagal. Daftarkan URL final.
  • Pengiriman bersifat at-least-once: event yang sama bisa tiba lebih dari sekali. Buat handler yang idempotent dan lakukan dedupe dengan id envelope.
  • Urutan tidak dijamin. Pakai status/previousStatus atau ambil ulang resource daripada mengandalkan urutan kedatangan.
  • Percobaan yang gagal diulang, total 8 percobaan, dengan exponential backoff mulai dari 30 detik.
  • Setelah 20 pengiriman gagal berturut-turut (semua percobaan habis), endpoint dinonaktifkan. Pengirimannya tetap ada di log; aktifkan lagi endpoint di konsol dan kirim ulang yang terlewat. Satu pengiriman yang berhasil me-reset penghitungnya.
  • Konsol menampilkan setiap pengiriman beserta status code dan potongan response; Kirim ulang mengirim satu pengiriman lagi dengan id event yang sama.