# Guide to KOMOJU Webhooks: Features and Best Practices

KOMOJU, run by Degica, is a payment gateway built for selling in Japan. Alongside cards, it supports methods Japanese customers expect, such as konbini (convenience-store) payments and bank transfers. Many of these are paid after checkout, sometimes days later at a shop counter, so a KOMOJU integration depends on webhooks to learn when an authorised payment is actually captured, expires or is refunded.

This guide covers KOMOJU's event list, the difference between dashboard and per-payment webhooks, verifying `X-Komoju-Signature` once you've set a secret token, and KOMOJU's long retry schedule.

## What are KOMOJU webhooks?

You create central webhooks in the KOMOJU dashboard under **Manage > Webhooks**, choosing the events each one receives. If you create payments through the API, you can also attach webhooks to an individual payment; KOMOJU sends those in addition to the central ones, not instead.

Every delivery is a JSON `POST` with the event name in both the `X-Komoju-Event` header and the body's `type`, a unique `X-Komoju-Id`, and the affected resource under `data`. A `ping` event arrives as soon as you create a webhook, so you can check your receiver straight away.

Signing is optional. Once you set a secret token on the webhook, KOMOJU adds `X-Komoju-Signature`, a hex HMAC-SHA256 of the raw body. Without a token, there's no signature header at all.

## KOMOJU webhook features

| Feature | Details |
| --- | --- |
| Configuration | Dashboard **Manage > Webhooks**, or per payment through the Payments API |
| Signature | hex HMAC-SHA256 |
| Signature header | `x-komoju-signature` |
| Signed content | The raw request body |
| Key | A secret token you choose (`secret_key` on per-payment webhooks) |
| Body | JSON |
| Event routing | Body field `type` (also the `X-Komoju-Event` header) |
| Delivery ID | `X-Komoju-Id` header; event `id` in the body |
| Retries | 25, with growing intervals, the last about 25 days later |
| Manual resend | From the payment's page in the dashboard |
| Source IPs | 9 published, but KOMOJU advises against IP allowlisting |

## Events and payloads

| Event | Triggered When | Common Use Cases |
| --- | --- | --- |
| `ping` | A test ping is sent (`data` is the webhook) | Check connectivity and signature setup |
| `payment.authorized` | A payment was authorized | Show konbini/bank-transfer instructions, hold stock |
| `payment.captured` | A payment was captured | Fulfil the order |
| `payment.updated` | A payment was changed | Sync status |
| `payment.expired` | A payment was expired | Release stock |
| `payment.cancelled` | A payment was cancelled | Cancel the order |
| `payment.refund.created` | A refund was created (may be partial) | Record a partial refund |
| `payment.refunded` | A payment was fully refunded | Mark the order refunded |
| `payment.failed` | A payment attempt failed (it may be re-attempted) | Prompt the customer to retry |
| `customer.created` | A customer was created | Sync customer records |
| `customer.updated` | A customer was updated | Sync customer records |
| `customer.deleted` | A customer was deleted | Remove customer records |
| `subscription.created` | A subscription was created | Start the subscription |
| `subscription.captured` | A subscription was captured | Extend access |
| `subscription.failed` | A subscription failed | Dunning |
| `subscription.suspended` | A subscription was suspended | Pause access |
| `subscription.deleted` | A subscription was deleted | End access |
| `settlement.created` | A settlement was created | Reconcile payouts |

KOMOJU's per-payment webhook documentation also mentions `payment.marked.as.fraud`, which isn't in the main events table, so it may not be available on central webhooks. Log any type you don't recognise rather than failing.

### Event Payload Structure

The body has an event `id`, the `type`, `resource: "event"`, the `data` object and `created_at`. `data.resource` names what `data` is: `payment` for payment events, `webhook` for a `ping`. KOMOJU's attribute table calls the top-level field `object`, but both of its example deliveries use `resource`, so code against the examples.

For a konbini or bank-transfer payment, `payment.authorized` carries the customer's payment instructions in `data.payment_details` (for a bank transfer, the account to pay into and an `instructions_url`), while `captured_at` stays `null` until the money arrives.

### Published payload example

Here is a `payment.authorized` delivery for a bank transfer, from the [webhook samples library](https://github.com/hookdeck/webhook-samples/blob/main/providers/komoju/latest/payment.authorized.json). Note the Japanese bank details and the `payment_deadline`.

```json
{
  "id": "dv7ywuavew3n2meqsllj5bbob",
  "type": "payment.authorized",
  "resource": "event",
  "data": {
    "id": "8xz5cn01gkt37cz0myd98k2f1",
    "resource": "payment",
    "status": "authorized",
    "amount": 1000,
    "tax": 80,
    "customer": null,
    "payment_deadline": "2018-11-20T14:59:59Z",
    "payment_details": {
      "type": "bank_transfer",
      "email": "test@example.com",
      "order_id": "Y5236584956",
      "bank_name": "三井住友銀行",
      "account_branch_name": "ひなぎく",
      "account_number": "6",
      "account_type": "普通預金",
      "account_name": "株式会社DEGICA（カブシキガイシャ デジカ）",
      "instructions_url": "https://komoju.com/ja/instructions/8xz5cn01gkt37cz0myd98k2f1"
    },
    "payment_method_fee": 0,
    "total": 1080,
    "currency": "JPY",
    "description": null,
    "captured_at": null,
    "external_order_num": "123",
    "metadata": {},
    "created_at": "2018-11-13T06:19:49Z",
    "amount_refunded": 0,
    "locale": "ja",
    "refunds": [],
    "refund_requests": []
  },
  "created_at": "2018-11-13T06:19:49Z"
}

```

## Setting up KOMOJU webhooks

### Prerequisites

* A KOMOJU account (a test mode account is available straight after sign-up)
* A public HTTPS endpoint that accepts `POST`

### Register Your Endpoint

1. Log in to the KOMOJU dashboard and go to **Manage > Webhooks**.
2. Add a webhook with your endpoint URL (for example `https://example.com/webhooks/komoju`).
3. Select the events to receive, such as `payment.captured`, `payment.refunded` and `payment.expired`.
4. Enter a secret token and save. KOMOJU sends a `ping` to the new webhook.

To attach a webhook to a single payment instead, pass it in the `webhooks` parameter when creating the payment through the API, with its own `secret_key`.

### Get Your Signing Secret

Set a secret token on the KOMOJU webhook. Without one, deliveries are unsigned and fail verification.

KOMOJU doesn't generate the token; you choose it when creating or editing the webhook. Store it as `WEBHOOK_SECRET` and use it as UTF-8 bytes, not decoded. Per-payment webhooks may use a different `secret_key`, so a receiver that accepts both needs to know which secret applies, or the two should share one.

### Test a delivery

Start a local receiver and forward requests with the [Hookdeck CLI](/docs/cli):

```bash
npx hookdeck-cli listen 3000 komoju --path /webhooks/komoju

```

Create a webhook in test mode with the CLI's URL; the `ping` arrives immediately. Then create a test konbini payment to see `payment.authorized` followed later by `payment.captured` or `payment.expired`.

## Securing KOMOJU webhooks

Compute HMAC-SHA256 of the raw body with the secret token, hex-encode it, and compare it with `X-Komoju-Signature` in constant time. KOMOJU's Ruby sample is `OpenSSL::HMAC.hexdigest('sha256', WEBHOOK_SECRET_TOKEN, request_body)`, and it warns that re-encoded JSON may not verify. See the [webhook documentation](https://docs.komoju.com/en/docs/introduction/core-concepts/webhooks).

```javascript
const crypto = require('node:crypto');

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const signature = headers['x-komoju-signature'];
  if (typeof signature !== 'string') return false;
  const key = Buffer.from(secret, 'utf8');
  const message = rawBody;
  const candidates = [signature];
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    if (!/^[0-9a-fA-F]{64}$/.test(candidate)) return false;
    const received = Buffer.from(candidate, 'hex');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

```

The handler deduplicates on the event `id` and treats only `payment.captured` as money received.

```javascript
const express = require('express');

function createKomojuApp({ alreadyProcessed, orders }) {
  const app = express();
  app.post('/webhooks/komoju', express.raw({ type: '*/*' }), async (req, res) => {
    const secret = process.env.WEBHOOK_SECRET;
    if (!secret) return res.sendStatus(500);
    if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);
    let parsed;
    try {
      parsed = JSON.parse(req.body.toString('utf8'));
    } catch {
      return res.sendStatus(400);
    }
    if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return res.sendStatus(400);
    const { id, type, data } = parsed;
    if (typeof type !== 'string') return res.sendStatus(400);
    if (type !== 'ping' && (!id || !data || typeof data !== 'object')) return res.sendStatus(400);
    try {
      if (type === 'ping' || (await alreadyProcessed(id))) return res.sendStatus(200);
      switch (type) {
        case 'payment.authorized':
          await orders.awaitPayment(data.id, data.payment_deadline);
          break;
        case 'payment.captured':
          await orders.markPaid(data.id, data.total, data.currency);
          break;
        case 'payment.expired':
        case 'payment.cancelled':
          await orders.release(data.id);
          break;
        case 'payment.refund.created':
        case 'payment.refunded':
          await orders.recordRefund(data.id, data.amount_refunded);
          break;
        default:
          console.log('Unhandled KOMOJU event', type);
      }
    } catch {
      return res.sendStatus(503);
    }
    return res.sendStatus(200);
  });
  return app;
}

```

No timestamp is signed, so the signature alone doesn't stop a replay. Storing the event `id` does.

Hookdeck's KOMOJU source type verifies `X-Komoju-Signature` against the raw body using the token you enter. KOMOJU always receives `200` from Hookdeck, so failed checks don't start KOMOJU's retries; they're recorded as failed verification and not forwarded. There's no timestamp tolerance. If a webhook has no secret token by design, leave verification off on the source.

> **Make KOMOJU webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) verifies every KOMOJU delivery, deduplicates retried events, and routes payment, subscription and settlement events to the services that need them.

## KOMOJU webhook limitations and pain points

### Unsigned by default

**The Problem:** A webhook created without a token accepts forged payment events if the receiver doesn't insist on a signature.

**Why It Happens:** KOMOJU only signs when a secret token is set, and the token field is optional.

**Workarounds:**

* Set a token on every central and per-payment webhook.
* Reject requests that lack `X-Komoju-Signature`.

**How Hookdeck Can Help:** A KOMOJU source with verification on records unsigned requests as failed and doesn't forward them.

### Two kinds of webhook, possibly two secrets

**The Problem:** Some deliveries fail verification, or a payment's events arrive twice.

**Why It Happens:** Per-payment webhooks are sent in addition to central ones and carry their own `secret_key`. A payment covered by both produces two deliveries of each event.

**Workarounds:**

* Use the same secret for both, or separate URLs.
* Deduplicate on the event `id`.

**How Hookdeck Can Help:** Separate Hookdeck sources for central and per-payment webhooks keep the secrets apart, and deduplication on `id` removes the overlap.

### Retries that last weeks

**The Problem:** A long-broken endpoint receives stale events days or weeks later, possibly out of order with newer ones.

**Why It Happens:** KOMOJU retries 25 times with growing intervals, the last about 25 days after the first attempt.

**Workarounds:**

* Check `created_at` and the current payment status before acting on an old event.
* Use the dashboard's resend from a payment's page for targeted recovery.

**How Hookdeck Can Help:** Hookdeck accepts the first attempt, so your downtime doesn't push events into KOMOJU's long tail, and you control when stored events are replayed.

### Conflicting envelope documentation

**The Problem:** Code written against the attribute table looks for a field that isn't there.

**Why It Happens:** The table names an `object` attribute, but the example deliveries have `resource`, plus an `id` the table omits.

**Workarounds:**

* Follow the examples, and read `type` rather than either field to route.

**How Hookdeck Can Help:** Every stored delivery in Hookdeck shows the actual body, which settles the question for your account.

## Best practices

### Wait for capture

For konbini and bank transfers, `payment.authorized` only means instructions were issued. Ship goods on `payment.captured` and release reserved stock on `payment.expired`.

### Don't rely on IP allowlisting

KOMOJU publishes nine sender addresses, but advises against IP-based access control and says the list may change without notice. The signature is the dependable check.

### Keep the body as bytes

Read the body with `express.raw`, `request.arrayBuffer()` or FastAPI's `request.body()` and verify before decoding. Japanese text in the body makes any re-encoding more likely to change bytes.

### Make processing idempotent

Retries, overlapping webhooks and dashboard resends all reuse the event `id`. Store it with the order change, as our [idempotency guide](/webhooks/guides/implement-webhook-idempotency) explains, and do slow work after acknowledging, as covered in [asynchronous webhook processing](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

KOMOJU's webhooks matter more than most because so many Japanese payment methods settle after checkout. Set a secret token on every webhook, route on `type`, wait for `payment.captured` before fulfilling, and deduplicate on `id` to absorb both the 25-retry schedule and any overlap with per-payment webhooks.

[Hookdeck Event Gateway](https://hookdeck.com) adds verification, deduplication and replay in front of your KOMOJU receiver.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle KOMOJU webhooks reliably in minutes.