1. Webhooks
Bunjang Open API
  • Overview
  • Changelog
  • Authentication
  • Product Catalog
    • Service Base URL
    • Sample Data
    • Full Catalog Download
      GET
    • Segment Catalog Download
      GET
  • Rest API
    • Service Base URL
    • Error Codes
    • Product Category
      • List Categories
    • Brand
      • List Brands
    • Product
      • Get Product
      • Filter On-Sale Products
      • Search Products
    • Point
      • Get Point Balance
      • Create Point Charge
      • Get Point Charge
      • Cancel Point Charge
    • Order
      • Create Order
      • Confirm Order
      • Get Order
      • List Orders
      • Create Order (Deprecated)
  • Webhooks
    • Overview
    • Signature Verification
    • Retry & Idempotency
    • Events
      • Order Status Changed
  • Schemas
    • Webhooks
      • WebhookEnvelope
      • WebhookOrderStatusChangedData
    • Product Condition
    • Product Active Sale Status
    • Order Status
  1. Webhooks

Signature Verification

Why verify signatures#

Webhook requests are sent from Bunjang's servers to your endpoint URL over the public internet. Without verification, any party that discovers your endpoint could send fabricated requests and trigger unwanted side effects in your system (state changes, notifications, downstream API calls).
Every webhook request includes an HMAC-SHA256 signature in the X-Bun-Webhook-Signature header, derived from a secret shared only between Bunjang and your team. By recomputing the signature on your side and comparing it against the header, you can be confident that:
1.
The request originated from Bunjang (authenticity).
2.
The payload was not tampered with in transit (integrity).
3.
The request is recent and not a replayed capture (freshness).
You must verify the signature on every webhook request before processing it. Requests with invalid or missing signatures should be rejected with 401 Unauthorized and not processed further.

About the signing secret#

Your webhook signing secret is provided as a Base64-encoded string (44 characters representing 256 bits of random key material). For example:
9hF2sK3pQ8mN5tR7vW1xY4zA6bC0dE2fG3hJ5kL7mN9=
You must Base64-decode this string to obtain the raw 32-byte key before using it with your HMAC function. Using the Base64 string directly as the HMAC key will produce signatures that do not match Bunjang's, and verification will always fail.
This is a common source of integration bugs — every code example below explicitly performs the Base64 decoding step.

Signature header format#

The X-Bun-Webhook-Signature header is a comma-separated list of key=value pairs:
X-Bun-Webhook-Signature: t=1735648123,v0=249e47edc1980531306517e4435b54ef1ff224020029284bdf19c8eda99aa325
KeyDescription
tUnix timestamp in seconds, representing when Bunjang generated the signature.
v0HMAC-SHA256 signature of the signed payload, hex-encoded.
About versioning: The v0 prefix denotes the current signature scheme version. If Bunjang ever changes the signing algorithm or signed payload format, a new version (v1, v2, ...) will be introduced alongside v0 during a transition period. Consumers should explicitly select the v0 value rather than assuming the header contains only one signature.

Verification steps#

Step 1. Extract and verify the timestamp#

Parse the t value from the signature header. Compare it against your server's current time and reject the request if the difference exceeds 5 minutes (300 seconds).
This protects against replay attacks where an attacker who has captured a valid past request attempts to resend it.
abs(currentEpochSeconds - t) > 300  → reject
Note: A 5-minute tolerance accommodates reasonable clock skew between Bunjang's servers and yours. Ensure your server's clock is synchronized via NTP; significant clock drift will cause legitimate requests to be rejected.

Step 2. Obtain the raw request body#

Use the exact raw bytes of the request body for verification. Do not parse the JSON and re-serialize it — JSON serialization is not byte-stable (key order, whitespace, number formatting may differ), and any difference will cause signature mismatch.
In most web frameworks, you must explicitly request the raw body before any JSON middleware consumes it. Common patterns:
Express (Node.js): Use express.raw({ type: 'application/json' }) for the webhook route, or capture the raw body with a verify callback in express.json().
Spring Boot (Kotlin/Java): Read the request body as String or ByteArray in the controller before any deserialization occurs.
FastAPI / Flask (Python): Use await request.body() (FastAPI) or request.get_data() with cache=True (Flask).

Step 3. Construct the base string#

Concatenate the version, timestamp, and raw body with colons:
{version}:{timestamp}:{raw_body}
Example:
v0:1735648123:{"eventId":"01kh07gag8ct3p7yb48kfgqwxt","eventType":"order.status.changed","payloadVersion":"v1","data":{"orderId":123456789,"orderItems":[{"orderItemId":1001,"pid":9999,"orderStatus":"PAYMENT_RECEIVED"}]}}

Step 4. Compute the HMAC-SHA256 signature#

Compute an HMAC-SHA256 of the base string using your Base64-decoded signing secret as the key.
ParameterValue
AlgorithmHMAC-SHA256
KeyBase64-decoded bytes of your webhook signing secret (32 bytes)
MessageThe base string from Step 3 (UTF-8 bytes)
OutputLowercase hex digest
Important: The signing secret is delivered as a Base64-encoded string. You must Base64-decode it to obtain the raw key bytes before passing it to your HMAC function. Skipping this step is the most common cause of signature mismatch.

Step 5. Compare signatures#

Compare your computed signature against the v0 value from the header. Use a constant-time comparison function to prevent timing attacks (e.g., crypto.timingSafeEqual in Node.js, hmac.compare_digest in Python, or a manual XOR-based loop in Kotlin).
If the signatures match exactly, the request is authentic and can be processed. Otherwise, reject the request with 401 Unauthorized.

Code examples#

Node.js (TypeScript)#

Kotlin (Spring Boot / Ktor)#

Python#


Common pitfalls#

Forgetting to Base64-decode the signing secret. The secret is delivered as a Base64-encoded string but must be decoded to raw bytes before use as the HMAC key. This is the single most common cause of signature mismatch.
Re-serializing the body before verification. Frameworks that automatically parse JSON will change the byte representation. Always work with the raw body bytes received over the wire.
Comparing signatures with ==. String equality is not constant-time in most languages, allowing timing-based attacks to gradually leak the expected signature. Always use a constant-time comparison.
Skipping the timestamp check. Even with a valid signature, an old captured request can be replayed if the timestamp is not verified.
Logging the signing secret. The signing secret must never appear in application logs, error reports, or version control. Store it in a secrets manager and load it at runtime.
Sharing the signing secret across environments. Use separate secrets for staging and production.

If signature verification fails#

1.
Confirm you are Base64-decoding the signing secret before using it as the HMAC key. This is the most common cause of failure.
2.
Confirm the secret matches the one provided by Bunjang during subscription. Secrets are environment-specific.
3.
Confirm you are using the raw request body, not a re-serialized version.
4.
Confirm your server clock is within 5 minutes of UTC (use NTP).
5.
Confirm you are reading the v0 value, not the full header string.
6.
If the issue persists, contact your Bunjang integration partner with a sample request (timestamp, signature header, and raw body) for debugging. Do not include the signing secret in your message.
Modified at 2026-06-04 07:21:37
Previous
Overview
Next
Retry & Idempotency
Built with