Merchant Lifecycle Events
Kesles sends webhooks to the bank's endpoint when a merchant's status changes. The bank must respond with 200 OK within 10 seconds.
This page documents outbound webhooks from Kesles to the bank (merchant lifecycle). For the reverse direction — forwarding payment events from the bank to Kesles — see the Events API below or the Endpoint List.
Webhook Signature Verification
Every lifecycle webhook from Kesles to the bank includes these headers:
X-Kesles-Signature: sha256=<signature>
X-Kesles-Timestamp: <unix_seconds>
X-Kesles-Event-ID: <uuid — for idempotency>
Verify the same way as for outbound requests (see Integration Contract).
Reject webhooks where |now - X-Kesles-Timestamp| > 300 seconds to prevent replay attacks.
Merchant Lifecycle Event Types
| Event | Trigger |
|---|---|
merchant.activated | Merchant activated for the first time (KYC verified + NMID assigned) |
merchant.suspended | Merchant suspended (KYC expired, violation, etc.) |
merchant.reactivated | Merchant reactivated after suspension |
merchant.terminated | Merchant permanently removed from the Kesles platform |
merchant.profile_updated | Name, address, or KYC data updated |
Lifecycle Payload Format
All lifecycle events use the same envelope:
{
"event_id": "evt-uuid-v4",
"event": "merchant.activated",
"occurred_at": "2026-05-21T10:00:00Z",
"merchant": {
"id": "kesles-merchant-uuid",
"nmid": "ID102326873XXXX",
"bank_code": "BMRI",
"merchant_name": "Warung Pak Budi",
"legal_name": "CV Sumber Rejeki",
"status": "active",
"kyc_status": "verified",
"category_code": "5411",
"phone": "081234567890",
"address": {
"city": "Makassar",
"province": "Sulawesi Selatan",
"country": "ID"
},
"updated_at": "2026-05-21T10:00:00Z"
},
"change": {
"field": "status",
"old_value": "inactive",
"new_value": "active",
"reason": "kyc_verified"
}
}
merchant.suspended
{
"event_id": "evt-uuid-v4",
"event": "merchant.suspended",
"occurred_at": "2026-05-21T11:00:00Z",
"merchant": { "...": "same as above" },
"change": {
"field": "status",
"old_value": "active",
"new_value": "suspended",
"reason": "kyc_expired"
}
}
reason values for merchant.suspended:
| Reason | Description |
|---|---|
kyc_expired | KYC documents expired |
violation | Terms of service violation |
operator_action | Manually suspended by a Kesles operator |
inactivity | No transactions for 6 months |
merchant.terminated
{
"event_id": "evt-uuid-v4",
"event": "merchant.terminated",
"occurred_at": "2026-05-21T12:00:00Z",
"merchant": {
"id": "kesles-merchant-uuid",
"nmid": "ID102326873XXXX",
"bank_code":"BMRI",
"status": "deleted"
},
"change": {
"field": "status",
"old_value": "active",
"new_value": "deleted",
"reason": "merchant_request"
}
}
After merchant.terminated, the NMID is no longer valid. The bank must reject transactions with that NMID and remove it from the local cache.
Payment Events API (Bank → Kesles)
The PSP forwards payment events to Kesles via the payment-service at POST /psp/v1/events.
Auth Headers (Events API)
X-PSP-Key-ID: {key_id}
X-PSP-Timestamp: {RFC3339 timestamp — e.g. 2026-06-08T10:30:00+07:00}
X-PSP-Signature: {raw hex signature — no prefix}
X-Request-ID: {uuid}
Signed Payload Format
Events API signed_payload: {method}\n{path}\n{timestamp}\n{body} — timestamp is the same RFC3339 value sent in X-PSP-Timestamp.
POST
/psp/v1/events
2026-06-08T10:30:00+07:00
{"external_event_id":"psp-evt-abc-123",...}
Payment Event Types
| event_type | Description |
|---|---|
transaction.success | New successful transaction at the bank |
transaction.failed | Transaction attempt failed |
transaction.cancelled | Transaction cancelled before completion |
transaction.refunded | Refund of a previous transaction |
transaction.reversed | Transaction reversed / voided |
qris.payment.success | Successful QRIS payment |
qris.payment.failed | Failed QRIS payment |
qris.payment.refunded | Refunded QRIS payment |
nmid.status_changed | Bank suspends or activates an NMID |
transaction.success Payload
{
"external_event_id": "psp-evt-abc-123",
"event_type": "transaction.success",
"nmid": "ID102326873XXXX",
"gross_amount": 75000,
"transaction_code": "TRX-PSP-20260523-001",
"reference_number": "BI20260523142345XXXX",
"rrn": "301234567890",
"transaction_at": "2026-05-23T14:23:45Z",
"payer_bank_code": "014",
"payer_bank_name": "BCA",
"payer_name_masked": "Budi S.",
"mdr_fee_bank": 300,
"switching_fee": 150,
"platform_fee": 75,
"status": "success"
}
nmid.status_changed Payload
{
"external_event_id": "psp-evt-nmid-789",
"event_type": "nmid.status_changed",
"nmid": "ID102326873XXXX",
"nmid_new_status": "suspended",
"nmid_old_status": "active",
"nmid_reason": "fraud_detected",
"transaction_at": "2026-05-23T12:00:00Z"
}
Idempotency (Lifecycle Webhooks from Kesles)
Use event_id from X-Kesles-Event-ID header for deduplication. If the same webhook is received twice, process it once — return 200 OK for both.
Retry from Kesles
If the bank's endpoint does not respond with 200 within 10 seconds, Kesles will retry:
| Attempt | Delay |
|---|---|
| 2 | 30 seconds |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
After 5 failed attempts, the Kesles team will send a manual notification to the technical PIC.
Webhook Endpoint Registration
Register your webhook URL with the Kesles team during onboarding. The URL must:
- Be HTTPS with a valid certificate
- Respond with
200 OKwithin 10 seconds - Accept
Content-Type: application/json