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. 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
- Log in to the KOMOJU dashboard and go to Manage > Webhooks.
- Add a webhook with your endpoint URL (for example
https://example.com/webhooks/komoju). - Select the events to receive, such as
payment.captured,payment.refundedandpayment.expired. - Enter a secret token and save. KOMOJU sends a
pingto 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_, 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_atand 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
typerather 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.