lollipayDocs

Authentication

Authenticate API requests with a secret key, sent as a bearer token or used to sign each request with HMAC-SHA256.

Every request is authenticated with an API key you create in Dashboard → Developers → API keys. A key belongs to one store, works in either test or live mode, and has an auth mode that is fixed when you create it.

Auth modeHow a request proves it's yoursChoose it when
BearerThe secret travels in the Authorization header.You want the simplest setup.
HMACThe secret signs the request and never travels.You want captured requests to be useless to anyone else.

Secrets look like lp_test_sk_… or lp_live_sk_…. They're shown once, when the key is created. Keep them in an environment variable on your server, never in a browser, a mobile app or a repository.

Anyone with a secret can act for your store within that key's permissions. If one leaks, revoke the key in the dashboard; requests with it get 401 straight away.

Bearer

Send the secret as a bearer token:

curl https://api.lollipay.cc/v1/stores/$LOLLIPAY_STORE_ID/checkouts/cs_55ca30f7581d9a76ac7188349f33b4811c4b43f28935ed507586d3bd92fa4982 \
  -H "Authorization: Bearer $LOLLIPAY_SECRET_KEY"

HMAC

An HMAC key comes with a key ID (a UUID, shown next to the secret when you create it). Instead of sending the secret, you sign every request with it and send three headers:

HeaderValue
Lollipay-Key-IdThe key ID.
Lollipay-TimestampThe current Unix time in seconds, like 1792315200.
Lollipay-SignatureThe lowercase hex HMAC-SHA256 of the string to sign, keyed with the secret.

The string to sign is four lines joined by \n:

String to sign
{timestamp}
{METHOD}
{path}
{hex(sha256(body))}
  • METHOD is upper case, like POST.
  • path is the request path without the query string, like /v1/stores/4f9c2d1e-…/checkouts.
  • The body hash is the SHA-256 of the exact bytes you send. For GET requests, hash the empty string (e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855).

Lollipay rebuilds the string from the request it received, so a signature only fits one method, one path and one body. Signatures with a timestamp more than 5 minutes away from our clock are refused, which keeps a captured request from being replayed later.

checkout.mjs
import { createHash, createHmac } from "node:crypto";

const { LOLLIPAY_KEY_ID, LOLLIPAY_SECRET_KEY, LOLLIPAY_STORE_ID } = process.env;

export async function lollipay(method, path, payload) {
  const body = payload === undefined ? "" : JSON.stringify(payload);
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const bodyHash = createHash("sha256").update(body).digest("hex");
  const signature = createHmac("sha256", LOLLIPAY_SECRET_KEY)
    .update(`${timestamp}\n${method}\n${path}\n${bodyHash}`)
    .digest("hex");

  const response = await fetch(`https://api.lollipay.cc${path}`, {
    method,
    headers: {
      "Content-Type": "application/json",
      "Lollipay-Key-Id": LOLLIPAY_KEY_ID,
      "Lollipay-Timestamp": timestamp,
      "Lollipay-Signature": signature,
    },
    body: body || undefined,
  });
  return response.json();
}

const session = await lollipay("POST", `/v1/stores/${LOLLIPAY_STORE_ID}/checkouts`, {
  items: [{ name: "Pro plan", amount: 4900 }],
  customer: { email: "buyer@example.com" },
  return_url: "https://example.com/thanks",
});

When a signature is refused

ResponseUsual cause
401 invalid_signatureA header is missing or malformed: the key ID isn't a UUID, the timestamp isn't in seconds, or the signature isn't 64 hex characters.
401 stale_signatureYour server's clock is more than 5 minutes off. Sync it with NTP.
401 unauthorizedThe signature doesn't match. Check that you hash the bytes you actually send (serialize the JSON once), that the path has no query string, and that the key isn't revoked.

Test and live keys

The key's mode decides the mode of everything it creates. Test keys create test sessions that are paid with test cards; live keys unlock once your business is verified and take real payments. A key can only read sessions of its own mode.

A store can have up to 10 active keys. Use one key per service so you can revoke one without touching the others.

On this page