Skip to main content

Security requirements

Every app installed on a DZBuild store holds a token to a merchant's orders, customers or catalogue. These rules apply to every app, in test mode and after approval. DZBuild can suspend an app that breaks them, and a suspended app gets 403 app_suspended on every call.

Redirect URIs​

Register 1 to 5 redirect URIs in the developer console. Each one must:

  • use https,
  • be at most 512 characters,
  • carry no user name, password or #fragment,
  • contain no * wildcard.

DZBuild compares the redirect_uri of each request with the registered list character for character. https://app.example.com/callback and https://app.example.com/callback/ are two different URIs. The console accepts https only, so test the install flow on a workers.dev address or through an https tunnel and register its address.

Generate a new random state for every authorize request, tie it to the merchant's session, and refuse a callback whose state does not match.

Client secret and access tokens​

The client secret (dzas_ followed by 48 hex characters) is shown once, when the app is created or the secret is rotated. DZBuild keeps only a hash of it. Use it on your server only: never in a browser, a mobile app, a public repository or a log line.

If the secret leaks, rotate it in the developer console. The old secret stops working at once, and access tokens already issued keep working.

Access tokens (dzpk_live_...) give the API access the merchant approved, with no expiry. Keep them server side, encrypted at rest, one per store. Apps have no endpoint to revoke a token. If a token leaks, ask the merchant to install your app again on that store: the new token replaces the old one at once. The merchant can also uninstall the app, which revokes it.

Verify webhook signatures​

Every webhook DZBuild sends to your app is signed with the app's signing secret, a 64-character hex string you can view and rotate in the console. The request carries X-DZ-Timestamp, X-DZ-Event, X-DZ-Delivery and:

X-DZ-Signature: t=1758880000,v1=5d41402abc4b2a76b9719d911017c592...

Compute HMAC-SHA256 with the signing secret over the timestamp, a dot and the raw request body, compare it with v1 in constant time, and reject a request whose timestamp is more than 5 minutes old. App endpoints never receive the X-DZ-Token header, so the signature is the only proof that a request came from DZBuild. The webhooks page has the full scheme and code in several languages.

One signing secret covers every store that installed your app. Rotating it re-keys all of them in one step.

Verify launch tokens​

When the merchant opens your app from the DZBuild dashboard, DZBuild redirects the browser to your launch URL with a dz_launch query parameter. The launch URL must use https. The parameter is a JWT signed with HS256, using the app's signing secret: the 64-character string exactly as the console shows it.

ClaimValue
issdzbuild
audYour client_id.
subThe id of the DZBuild user who opened the app, as a string.
store_idThe store the app was opened from.
install_idYour install on that store.
is_ownertrue for the store owner, false for a team member.
iatIssue time, Unix seconds.
expiat plus 300 seconds.
jti16 random hex characters.

Accept the token only when the signature matches, the header says HS256, iss is dzbuild, aud is your client id and exp has not passed. Then refuse any jti you have already accepted in the last 5 minutes. After reading the token, redirect to a URL without it so it stays out of browser history and logs.

A launch token proves who opened the app and from which store. It does not call the API: use the access token you stored for that store_id and install_id.

In PHP:

<?php
/** Claims of a valid dz_launch token, or null. $clientId is your app's client_id. */
function verifyLaunchToken(string $jwt, string $signingSecret, string $clientId): ?array
{
$parts = explode('.', $jwt);
if (count($parts) !== 3) {
return null;
}
[$head, $body, $sig] = $parts;
$b64 = static fn(string $s): string|false => base64_decode(strtr($s, '-_', '+/'), true);
$expected = rtrim(strtr(base64_encode(hash_hmac('sha256', "$head.$body", $signingSecret, true)), '+/', '-_'), '=');
if (!hash_equals($expected, $sig)) {
return null;
}
$header = json_decode((string) $b64($head), true);
$claims = json_decode((string) $b64($body), true);
if (($header['alg'] ?? '') !== 'HS256' || !is_array($claims)) {
return null;
}
$now = time();
if (($claims['iss'] ?? '') !== 'dzbuild' || ($claims['aud'] ?? '') !== $clientId
|| !is_int($claims['exp'] ?? null) || $claims['exp'] < $now || ($claims['iat'] ?? 0) > $now + 60) {
return null;
}
return $claims; // Then refuse a jti you have already seen in the last 5 minutes.
}

In Node.js:

const crypto = require('node:crypto');

// Claims of a valid dz_launch token, or null. clientId is your app's client_id.
function verifyLaunchToken(jwt, signingSecret, clientId) {
const parts = String(jwt).split('.');
if (parts.length !== 3) return null;
const [head, body, sig] = parts;
const expected = crypto.createHmac('sha256', signingSecret).update(`${head}.${body}`).digest();
const given = Buffer.from(sig, 'base64url');
if (given.length !== expected.length || !crypto.timingSafeEqual(given, expected)) return null;
const header = JSON.parse(Buffer.from(head, 'base64url').toString('utf8'));
const claims = JSON.parse(Buffer.from(body, 'base64url').toString('utf8'));
const now = Math.floor(Date.now() / 1000);
if (header.alg !== 'HS256' || claims.iss !== 'dzbuild' || claims.aud !== clientId) return null;
if (!Number.isInteger(claims.exp) || claims.exp < now || claims.iat > now + 60) return null;
return claims; // Then refuse a jti you have already seen in the last 5 minutes.
}

Delete store data after uninstall​

When a merchant uninstalls your app, DZBuild revokes the store's token, drops the webhook deliveries still waiting for that install, and sends one app.uninstalled event to your webhook URL when it is verified and active:

{
"id": "evt_...",
"event": "app.uninstalled",
"created_at": "2026-09-26T10:15:00+01:00",
"store_id": 141,
"data": {
"install_id": 57,
"client_id": "dzapp_0123456789abcdef0123",
"store_id": 141,
"uninstalled_at": "2026-09-26T09:15:00+00:00"
}
}

Delete the data you hold for that store within 30 days of the event: orders, customers, products and every copy of the access token. If your app has no verified webhook URL, a 401 on the store's token is your signal.

Ask for the fewest scopes​

Request only the scopes your features use: no write scope on data you only read, no customers:read for an app that never shows a customer. DZBuild compares the requested scopes with what your listing says the app does, and the merchant sees one line per resource on the consent screen before approving.

No scraping​

Reach store data only through the REST API and webhooks. Do not script the DZBuild dashboard, sign in as the merchant, or scrape dashboard or storefront pages. Never ask a merchant for their DZBuild password or for a merchant API key.

Answer support email​

Register a support email address that you read, and answer merchants who write to it. The consent screen and the app page in the merchant dashboard show it. DZBuild sends review decisions to your account email, not to this address.

This page for AI toolsView as MarkdownOpen in ChatGPTOpen in Claude