LINE webhook signature verification

LINE signs its webhook with a base64 HMAC-SHA256 of the raw body in X-Line-Signature. Paste the header, body and channel secret to check it.

Last reviewed · scheme facts from the site's provider table · LINE documentation

Check a LINE signature

Paste the provider's signature header as a JSON object. Header names are matched case-insensitively.

The secret is sent to AnyHook's own API to compute the HMAC. It is not stored or logged, and the result only ever shows masked signatures.

The exact rule

These are the facts this verifier implements, read from the same provider table that powers /providers. A mismatch here is a bug in the tool, not a preference.

Signature header
x-line-signature = base64
Algorithm
HMAC-SHA256
Signed string
raw body
Encoding
base64
Timestamp / tolerance
Not published
Signing secret
Channel secret

How LINE signs a webhook

The header is `X-Line-Signature: <base64>`. LINE computes HMAC-SHA256 over the raw request body with the channel secret, then base64-encodes the digest. There is no prefix and no timestamp component, so the verifier reads one header and compares the encoded digest in constant time.

The body is JSON, often with nested events. Parse it after verifying, never before. LINE batches events in one delivery, so the signature covers the whole batch, including any events your handler later ignores.

Multi-channel deployments need care: each Messaging API channel has its own channel secret, and a delivery signed for one channel will not verify with another's secret. Select the secret by the channel or bot the delivery belongs to before comparing, and keep that mapping next to the handler rather than in the channel configuration alone.

Which secret to use, and what the timestamp means

The secret is the channel secret from the LINE Developers console: your provider → channel → Messaging API → Channel secret. It is not the channel access token (which authorises calls to the Messaging API) and not the channel ID. Copying the token into the verifier is the classic mismatch.

LINE publishes no timestamp or tolerance for this signature in the provider table this site maintains, and this tool therefore checks only the HMAC. If replay is a risk for your handler, use the event ids and timestamps inside the payload for deduplication; the header will not do it for you.

Read the channel secret from the environment at verification time and keep it out of client-side code, logs and crash reports. Because the check is cheap — one HMAC and one comparison — it belongs before any database work, any call to the LINE reply API and any parse of the event list. A request that fails the HMAC should be answered and forgotten; there is nothing to dispatch and nothing worth logging beyond a counter.

The raw body is the input

The channel secret signs the exact body bytes. In Next.js use `await request.text()`; in Express use `express.raw({ type: "application/json" })`; in PHP read `php://input`. A JSON parse followed by re-serialisation changes key order, spacing and escaping, and the signature no longer matches.

Because LINE's signature covers the body only, compression or proxy rewrites before your handler see the request are fatal. Verify at the same point in the stack where the raw body is still available, and keep the raw string for logging without the secret.

One delivery usually contains a batch of events: messages, follow events, postbacks and so on. The signature covers the entire batch, so a single malformed byte anywhere invalidates it for every event inside. Verify once at the edge of your handler, then dispatch the events; do not re-verify per event after the parsed objects have been through your own transformations.

The header does not name the channel, so the receiver decides which channel secret to use. In a single-channel integration that is unambiguous; in a multi-channel one, read the destination field after verification and route the batch to the right handler. A signature made with channel A's secret will verify only with channel A's secret, which is also a useful way to catch a misrouted delivery.

A sample signature header

The signature below is real for this sample body and the made-up channel secret above. Load a fresh sample in the tool to generate a complete set that verifies.

Header(s)

{
  "x-line-signature": "ZABWUiaadVo+wp2TXyysHWK83zmERCutpL4kGxOgLo0="
}

Raw body

{"destination":"Uexample","events":[{"type":"message","message":{"type":"text","id":"1","text":"hello"},"timestamp":1700000000000}]}

Example secret (not a real credential)

line_example_channel_secret_not_real

Verify it in your own server

Three complete implementations of the same rule. Each reads the exact header(s), HMACs the raw body with the secret and compares in constant time; the comment at the top says where the raw body comes from in that runtime.

Node.js

import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: the exact bytes LINE sent. signatureHeader: x-line-signature.
export function verifyLineSignature(rawBody, signatureHeader, channelSecret) {
  const expected = createHmac("sha256", channelSecret).update(rawBody).digest("base64");
  const expectedBuffer = Buffer.from(expected);
  const providedBuffer = Buffer.from(signatureHeader);
  return (
    expectedBuffer.length === providedBuffer.length &&
    timingSafeEqual(expectedBuffer, providedBuffer)
  );
}

Python

import base64
import hashlib
import hmac

def verify_line_signature(raw_body: bytes, signature_header: str, channel_secret: str) -> bool:
    digest = hmac.new(channel_secret.encode(), raw_body, hashlib.sha256).digest()
    expected = base64.b64encode(digest)
    return hmac.compare_digest(expected, signature_header.encode())

PHP

<?php
// $rawBody = file_get_contents("php://input");
function verify_line_signature(string $rawBody, string $signatureHeader, string $channelSecret): bool {
    $expected = base64_encode(hash_hmac("sha256", $rawBody, $channelSecret, true));
    return hash_equals($expected, $signatureHeader);
}

Common LINE failures

Each row is a failure seen while verifying this scheme, not a generic webhook checklist. The symptom column is what the checker or your handler reports; the fix is the change that resolves it.

MistakeSymptomFix
Channel access token used as the channel secretThe HMAC never matches; the token is valid for the Messaging API.Use the Channel secret from the Messaging API tab, not the channel access token.
Base64 compared against hexThe two values look unrelated even when the body and secret are right.Base64-encode the digest; LINE sends base64, padding included.
Body parsed before verificationThe tool passes with a pasted body, the handler rejects the same delivery.Read and HMAC the raw body before JSON parsing.
Wrong channel's secret in a multi-channel setupOnly deliveries from one channel verify; the rest fail with a correct-looking body.Choose the channel secret per channel/bot before verifying.
Re-serialised Unicode or escaped charactersFails on payloads with non-ASCII text, passes on plain English tests.HMAC the bytes as received; do not normalise or unescape first.
Trimmed trailing newline or added bytesA replay of the raw body verifies, the live request does not.Check length at the handler and look for proxies or middleware that touch the body.
Missing header treated as a valid signatureA handler's empty-string compare passes under some conditions.Reject first when the header is absent, then compare.
Non-constant-time comparisonTests pass; the endpoint leaks timing.Use `timingSafeEqual`, `hmac.compare_digest` or `hash_equals` after checking lengths.

LINE signature FAQ

Where is the LINE channel secret?
LINE Developers console → your channel → Messaging API tab → Channel secret. It sits next to the channel access token, which is a different value with different uses.
Does LINE send a timestamp in the signature?
No. The header is a base64 HMAC of the body only, and LINE publishes no tolerance for it. Replay protection has to come from the event ids inside the payload.
Why base64 instead of hex?
That is the encoding LINE chose. The digest is the same HMAC-SHA256; only the representation differs. Compare the encoded strings in constant time.
Can one secret verify several bots?
No. Each Messaging API channel has its own channel secret; a delivery for one channel will not verify with another channel's secret.
Can I verify LINE signatures without the LINE SDK?
Yes. The samples use only each language's standard crypto library. There is no network call and no SDK dependency in the verifier itself.
Does this tool call LINE's API?
No. It computes an HMAC locally in AnyHook's API route and returns a masked result; it does not forward the payload or secret anywhere else.

What this tool cannot tell you

  • It cannot prove LINE sent the request; a valid HMAC proves the body was signed with the channel secret.
  • It cannot detect replay: LINE's scheme has no timestamp, so deduplicate on the payload's event ids yourself.
  • It cannot check that the payload's destination or channel fields belong to your bot; verify those after parsing.
  • It cannot recover a body that was parsed or rewritten before the check.

Next checks

A valid signature does not mean the delivery succeeded. Time your handler against each sender's response budget, and keep the event so a failure can be replayed.