API and price webhook
Last updated 10 November 2026
Your site is not on a platform we list? Any stack works. There are three ways to get a CostCal Pro price into your own code. Use one or all.
1. Price webhook (we push to you)
In the app, open Stores, choose Custom site and enter an https address on your site. We show you a signing secret once. When you press Send price to my site (Premium and above), we POST a JSON message to that address. Your code checks the signature, then updates your own database.
POST /hooks/costcal HTTP/1.1
Content-Type: application/json
X-CostCal-Event: price.update
X-CostCal-Timestamp: 1790000000
X-CostCal-Signature: 9b1c...e4 (hex HMAC-SHA256 of "1790000000." + body)
X-CostCal-Delivery: 7f0e6d6e-6f0c-4b0c-9f0a-1d2e3f4a5b6c
{"event":"price.update","sku":"MUG-01","name":"Mug","price":"14.29","currency":"USD","calculationId":"c5c0..."}- The signature is the hex HMAC-SHA256 of
timestamp + "." + raw body, using your signing secret. - Reject a request when the timestamp is more than 5 minutes away from your clock. That stops replays of an old message.
- Compare signatures in constant time, and sign the raw body, not re-serialised JSON.
- Answer with any 2xx status. We do not follow redirects, wait at most 8 seconds, and do not retry automatically.
X-CostCal-Deliveryis a unique id per message. Store it if you want to ignore duplicates.- Your address must be https on port 443 and resolve to a public IP address. Private, loopback and link-local addresses are refused.
- The Test button sends
{"event":"ping"}with the same headers.
Node.js (Express)
import crypto from "node:crypto";
import express from "express";
const app = express();
// Keep the raw body: the signature covers the exact bytes we sent.
app.post("/hooks/costcal", express.raw({ type: "application/json" }), (req, res) => {
const ts = req.header("X-CostCal-Timestamp") ?? "";
const sig = req.header("X-CostCal-Signature") ?? "";
const body = req.body.toString("utf8");
if (!/^\d{9,12}$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(400);
const want = crypto.createHmac("sha256", process.env.COSTCAL_SECRET).update(ts + "." + body).digest("hex");
const a = Buffer.from(sig, "hex");
const b = Buffer.from(want, "hex");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);
const event = JSON.parse(body); // { event, sku, name, price, currency, calculationId }
if (event.event === "price.update") {
// update your own database here, for example: UPDATE products SET price = event.price WHERE sku = event.sku
}
res.sendStatus(200);
});
app.listen(3000);
PHP
<?php
// hooks/costcal.php
$body = file_get_contents('php://input'); // raw body
$ts = $_SERVER['HTTP_X_COSTCAL_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_COSTCAL_SIGNATURE'] ?? '';
if (!ctype_digit($ts) || abs(time() - (int) $ts) > 300) { http_response_code(400); exit; }
$want = hash_hmac('sha256', $ts . '.' . $body, getenv('COSTCAL_SECRET'));
if (!hash_equals($want, $sig)) { http_response_code(401); exit; }
$event = json_decode($body, true); // event, sku, name, price, currency, calculationId
if (($event['event'] ?? '') === 'price.update') {
// update your own database here, for example with a prepared statement keyed on $event['sku']
}
http_response_code(200);
Python (Flask)
import hashlib
import hmac
import json
import os
import time
from flask import Flask, abort, request
app = Flask(__name__)
@app.post("/hooks/costcal")
def costcal():
body = request.get_data() # raw bytes
ts = request.headers.get("X-CostCal-Timestamp", "")
sig = request.headers.get("X-CostCal-Signature", "")
if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
abort(400)
want = hmac.new(os.environ["COSTCAL_SECRET"].encode(), ts.encode() + b"." + body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(want, sig):
abort(401)
event = json.loads(body) # event, sku, name, price, currency, calculationId
if event.get("event") == "price.update":
pass # update your own database here, keyed on event["sku"]
return "", 200
2. Pull API (you fetch from us)
Create a personal API key under API keys (Starter and above). Send it as a bearer token. Keys are shown once, stored only as a hash, scoped, and can be revoked at any time.
curl -H "Authorization: Bearer $COSTCAL_KEY" "https://costcalpro.shop/api/v1/prices?sku=MUG-01"
curl -H "Authorization: Bearer $COSTCAL_KEY" "https://costcalpro.shop/api/v1/prices?format=csv" -o prices.csv
curl -H "Authorization: Bearer $COSTCAL_KEY" "https://costcalpro.shop/api/v1/calculations?limit=50"GET /api/v1/pricesneeds theprices:readscope. Add?sku=ABCfor one SKU (404 when unknown) or?format=csv.GET /api/v1/calculationsneedscalculations:read. Uselimit(up to 200) andoffset.- Prices are decimal strings in your account currency, for example
"14.29". - Rate limit per key: 60 requests a minute on Starter, 120 on Premium, 300 on Agency. Over the limit you get 429 and a
Retry-Afterheader.X-RateLimit-Remainingis sent on every answer. - Errors look like
{"error":{"code":"invalid_key","message":"..."}}with status 401 (missing, wrong or revoked key), 403 (plan or scope) or 429.
3. CSV or JSON export
In Stores, the Price list card downloads your prices as CSV or JSON (SKU, name, price, currency). This works on every paid plan. For a stable address that a cron job can fetch, use the pull API with ?format=csv.