Gareth Wilson Gareth Wilson

Guide to KOMOJU Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationDashboard Manage > Webhooks, or per payment through the Payments API
Signaturehex HMAC-SHA256
Signature headerx-komoju-signature
Signed contentThe raw request body
KeyA secret token you choose (secret_key on per-payment webhooks)
BodyJSON
Event routingBody field type (also the X-Komoju-Event header)
Delivery IDX-Komoju-Id header; event id in the body
Retries25, with growing intervals, the last about 25 days later
Manual resendFrom the payment's page in the dashboard
Source IPs9 published, but KOMOJU advises against IP allowlisting

Events and payloads

EventTriggered WhenCommon Use Cases
pingA test ping is sent (data is the webhook)Check connectivity and signature setup
payment.authorizedA payment was authorizedShow konbini/bank-transfer instructions, hold stock
payment.capturedA payment was capturedFulfil the order
payment.updatedA payment was changedSync status
payment.expiredA payment was expiredRelease stock
payment.cancelledA payment was cancelledCancel the order
payment.refund.createdA refund was created (may be partial)Record a partial refund
payment.refundedA payment was fully refundedMark the order refunded
payment.failedA payment attempt failed (it may be re-attempted)Prompt the customer to retry
customer.createdA customer was createdSync customer records
customer.updatedA customer was updatedSync customer records
customer.deletedA customer was deletedRemove customer records
subscription.createdA subscription was createdStart the subscription
subscription.capturedA subscription was capturedExtend access
subscription.failedA subscription failedDunning
subscription.suspendedA subscription was suspendedPause access
subscription.deletedA subscription was deletedEnd access
settlement.createdA settlement was createdReconcile 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. Note the Japanese bank details and the payment_deadline.

{
  "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:

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.

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.

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 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 explains, and do slow work after acknowledging, as covered in asynchronous webhook processing.

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 adds verification, deduplication and replay in front of your KOMOJU receiver.

Get started with Hookdeck for free and handle KOMOJU webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.