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 mode | How a request proves it's yours | Choose it when |
|---|---|---|
| Bearer | The secret travels in the Authorization header. | You want the simplest setup. |
| HMAC | The 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:
| Header | Value |
|---|---|
Lollipay-Key-Id | The key ID. |
Lollipay-Timestamp | The current Unix time in seconds, like 1792315200. |
Lollipay-Signature | The lowercase hex HMAC-SHA256 of the string to sign, keyed with the secret. |
The string to sign is four lines joined by \n:
{timestamp}
{METHOD}
{path}
{hex(sha256(body))}METHODis upper case, likePOST.pathis 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
GETrequests, 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.
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
| Response | Usual cause |
|---|---|
401 invalid_signature | A 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_signature | Your server's clock is more than 5 minutes off. Sync it with NTP. |
401 unauthorized | The 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.