Skip to content

Signed return URLs

After a payment, GIEP sends the shopper back to your return page with giep_* query parameters (SELL-047). With a store signing secret (Stores & integrations > Integrations, shown once when generated) the URL is signed:

Parameter Meaning
giep_session checkout session id
giep_status paid, failed or cancelled
giep_final 1 final (paid, cancelled), 0 not final (failed: the shopper may still pay)
giep_order GIEP order id (paid only)
giep_amount, giep_currency total in minor units, ISO 4217
giep_ref your order reference, when you sent one
giep_ts unix seconds when the URL was built
giep_sig hex HMAC-SHA256 with your current secret
giep_sig_prev the same with your previous secret, only for 24 h after a rotation

Algorithm: take every giep_* parameter except giep_sig and giep_sig_prev, sort by name, join as name=value with & where each value is percent-encoded like JavaScript's encodeURIComponent, HMAC-SHA256 it with your secret, and compare in constant time with giep_sig or giep_sig_prev. Reject a giep_ts more than 5 minutes away. The redirect is a pointer, never proof of payment: fulfil only on the order.paid webhook or GET /v1/checkout-sessions/:id.

Node.js

JavaScript
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyGiepReturn(url, secret, maxAgeSeconds = 300) {
  const q = new URL(url).searchParams;
  const params = [...q].filter(([k]) => k.startsWith('giep_') && k !== 'giep_sig' && k !== 'giep_sig_prev');
  const canonical = params
    .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
    .map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
    .join('&');
  const expected = createHmac('sha256', secret).update(canonical).digest();
  const ok = [q.get('giep_sig'), q.get('giep_sig_prev')].some((sig) => {
    if (!sig || !/^[0-9a-f]{64}$/.test(sig)) return false;
    const given = Buffer.from(sig, 'hex');
    return given.length === expected.length && timingSafeEqual(given, expected);
  });
  const ts = Number(q.get('giep_ts'));
  return ok && Number.isSafeInteger(ts) && Math.abs(Date.now() / 1000 - ts) <= maxAgeSeconds;
}

PHP

PHP
function giep_verify_return(string $url, string $secret, int $maxAge = 300): bool {
    parse_str((string) parse_url($url, PHP_URL_QUERY), $q);
    $params = [];
    foreach ($q as $k => $v) {
        if (strpos($k, 'giep_') === 0 && $k !== 'giep_sig' && $k !== 'giep_sig_prev') $params[$k] = (string) $v;
    }
    ksort($params, SORT_STRING);
    $parts = [];
    // rawurlencode differs from encodeURIComponent only for !'()*, which GIEP values never contain.
    foreach ($params as $k => $v) $parts[] = $k . '=' . rawurlencode($v);
    $expected = hash_hmac('sha256', implode('&', $parts), $secret);
    $ok = false;
    foreach (['giep_sig', 'giep_sig_prev'] as $name) {
        if (isset($q[$name]) && is_string($q[$name]) && hash_equals($expected, $q[$name])) $ok = true;
    }
    $ts = isset($q['giep_ts']) ? (int) $q['giep_ts'] : 0;
    return $ok && abs(time() - $ts) <= $maxAge;
}

Python

Python
import hashlib, hmac, time
from urllib.parse import parse_qsl, quote, urlparse

def verify_giep_return(url: str, secret: str, max_age: int = 300) -> bool:
    q = dict(parse_qsl(urlparse(url).query, keep_blank_values=True))
    params = sorted((k, v) for k, v in q.items()
                    if k.startswith("giep_") and k not in ("giep_sig", "giep_sig_prev"))
    # safe="-_.!~*'()" matches JavaScript's encodeURIComponent.
    canonical = "&".join(f"{k}={quote(v, safe=\"-_.!~*'()\")}" for k, v in params)
    expected = hmac.new(secret.encode(), canonical.encode(), hashlib.sha256).hexdigest()
    ok = any(hmac.compare_digest(expected, q.get(n, "")) for n in ("giep_sig", "giep_sig_prev"))
    ts = int(q.get("giep_ts", "0") or 0)
    return ok and abs(time.time() - ts) <= max_age

Rotation

Rotate in Stores & integrations > Integrations: the new secret signs at once (giep_sig) and the previous one keeps signing (giep_sig_prev) for 24 hours. Update your shop to the new secret within that window; code like the samples above, which accepts either parameter, sees no failed verification.