Skip to content

Commit 031901d

Browse files
garethxclaude
andauthored
docs(shopline): signature is lowercase hex, not base64 — verified against a live delivery (#150)
SHOPLINE's docs show a base64 digest in the header example and a code sample showing hex; the brief told readers to assume base64. A live products/create delivery (api_version v20240601) settles it the other way: the header was 64 lowercase hex characters and matched a recomputed HMAC-SHA256 over the raw body with the app secret exactly. Handlers still accept both encodings, since the documented example disagrees with observed behaviour, but hex is now documented as what to expect. Also captured from the same delivery: api_version is required in the webhook-creation body (400 without it) and is echoed back in X-Shopline-Api-Version. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent d7e6c61 commit 031901d

3 files changed

Lines changed: 28 additions & 12 deletions

File tree

providers.yaml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1928,13 +1928,18 @@ providers:
19281928
E-commerce platform. The SHOPLINE Open Platform (developer.shopline.com, English) is a
19291929
Shopify clone. Webhooks are signed with HMAC-SHA256 of the RAW request body keyed by the
19301930
app secret (Developer Center > App credentials); the signature is in the
1931-
X-Shopline-Hmac-Sha256 header. Encoding is base64 (the docs' header example is base64,
1932-
Shopify-style — though a stray code sample shows hex; treat as base64 and fall back to hex
1933-
if needed). Constant-time compare against the raw un-parsed body. This is NOT the Standard
1931+
X-Shopline-Hmac-Sha256 header. *** ENCODING VERIFIED (Jul 2026, live products/create
1932+
delivery on api_version v20240601): the digest is LOWERCASE HEX, 64 chars — confirmed by
1933+
recomputing HMAC-SHA256 over the raw body with the app secret and matching the header
1934+
exactly. The docs' header example shows base64 (Shopify-style) and is MISLEADING; the
1935+
"stray" hex code sample is the correct one. Handlers may accept both as a safety net, but
1936+
expect hex. *** Constant-time compare against the raw un-parsed body. This is NOT the Standard
19341937
Webhooks spec. Other headers: X-Shopline-Topic, X-Shopline-Shop-Domain, X-Shopline-Shop-Id,
19351938
X-Shopline-Merchant-Id, X-Shopline-API-Version, X-Shopline-Webhook-Id (stable across resends,
19361939
use for idempotency). Topics use Shopify-style slash format (orders/create, products/update,
1937-
collect/delete). Subscribe via the Admin REST API
1940+
collect/delete). NOTE: api_version is REQUIRED in the webhook-creation body — omitting it
1941+
returns 400 "The required property 'api_version' is missing from the object"; deliveries echo
1942+
it back in X-Shopline-Api-Version. Subscribe via the Admin REST API
19381943
POST https://{handle}.myshopline.com/admin/openapi/{version}/webhooks.json with
19391944
{topic, address, api_version}. Gotchas: 5s response timeout; up to 19 retries over 48h then
19401945
the subscription is auto-removed. No official published SDK — verify manually.

skills/shopline-webhooks/SKILL.md

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,18 @@ secret** (Developer Center → App credentials) and sends the digest in the
2828
`X-Shopline-Hmac-Sha256` header. Use the **raw** body — parsing JSON first
2929
changes the bytes and breaks the signature — and compare timing-safe.
3030

31-
> **Encoding:** SHOPLINE's docs show a **base64** digest (Shopify-style), but a
32-
> stray code sample shows **hex**. To be safe, accept either: compute both and
33-
> timing-safe compare against each. Then **confirm which encoding your real
34-
> deliveries use** — log one live `X-Shopline-Hmac-Sha256` value (44 chars ending
35-
> in `=` is base64; 64 `[a-f0-9]` chars is hex) — and you can narrow the check.
31+
> **Encoding — verified as lowercase hex.** SHOPLINE's docs show a **base64**
32+
> digest in the header example (Shopify-style), while a code sample shows **hex**.
33+
> A live delivery settles it: the code sample is right. Confirmed against a real
34+
> `products/create` webhook (API version `v20240601`) by recomputing
35+
> HMAC-SHA256 over the raw body with the app secret — the header was 64 lowercase
36+
> hex characters and matched exactly.
37+
>
38+
> The handlers below still accept either encoding, since the documented example
39+
> disagrees with observed behaviour and SHOPLINE could differ by version or
40+
> region — but expect hex. To check your own: 64 `[a-f0-9]` chars is hex; 44
41+
> chars ending `=` is base64.
42+
>
3643
> The topic is in `X-Shopline-Topic`; the shop domain in `X-Shopline-Shop-Domain`.
3744
3845
Node:
@@ -43,8 +50,8 @@ const crypto = require('crypto');
4350
function verifyShoplineWebhook(rawBody, hmacHeader, secret) {
4451
if (!hmacHeader) return false;
4552
const digest = crypto.createHmac('sha256', secret).update(rawBody).digest();
46-
// Accept base64 (documented) or hex (stray sample) — timing-safe either way.
47-
return [digest.toString('base64'), digest.toString('hex')].some((expected) => {
53+
// Verified hex in practice; base64 kept as a fallback. Timing-safe either way.
54+
return [digest.toString('hex'), digest.toString('base64')].some((expected) => {
4855
try {
4956
return crypto.timingSafeEqual(Buffer.from(hmacHeader), Buffer.from(expected));
5057
} catch {
@@ -63,7 +70,7 @@ def verify_shopline_webhook(raw_body: bytes, hmac_header: str, secret: str) -> b
6370
if not hmac_header:
6471
return False
6572
digest = hmac.new(secret.encode(), raw_body, hashlib.sha256).digest()
66-
# Accept base64 (documented) or hex (stray sample).
73+
# Verified hex in practice; base64 kept as a fallback.
6774
return (
6875
hmac.compare_digest(hmac_header, base64.b64encode(digest).decode())
6976
or hmac.compare_digest(hmac_header, digest.hex())

skills/shopline-webhooks/references/setup.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ curl -X POST \
4343
}'
4444
```
4545

46+
> **`api_version` is required in the create body.** Omitting it returns
47+
> `400 body.webhook:The required property 'api_version' is missing from the object`.
48+
> Deliveries then echo it back in the `X-Shopline-Api-Version` header.
49+
4650
Repeat for each topic you want (`orders/create`, `products/update`,
4751
`collect/delete`, etc.). See
4852
[Subscribe to a Webhook](https://developer.shopline.com/docs/apps/api-instructions-for-use/webhooks/overview/)

0 commit comments

Comments
 (0)