Pembangun

4pricing API for developers

Connect your online shop, ERP or spreadsheet to 4pricing: read your competitors' prices, send your own prices and sales, and receive price changes through a signed webhook. You do not need to be a programmer to start — the quick start below gives you a ready-made file you only have to upload.

Base URL: https://4pricing.net/api/v1/pricing · OpenAPI specification (JSON)

Quick integration in 3 steps

This is the shortest way to see your 4pricing data on your own website. You will copy one file, paste your key into it and upload it to your hosting — no framework, no installation.

What you need
  • An active 4pricing subscription (the API is included in every plan).
  • Hosting with PHP — almost every shared hosting has it (cPanel, Plesk and similar).
  • A way to upload a file: your host's File Manager or an FTP program.
  1. Step 1: Create your API key

    Sign in to panel.4pricing.net and open Price Monitoring → API & keys.

    Click “Generate key”, give it a name (for example “my shop”) and copy the key that starts with prc_live_. It is shown only once — if you lose it, create another one and revoke the old one.

    Open API & keys in the panel →

  2. Step 2: Copy the ready-made file and paste your key

    Copy the PHP file below into a text editor (Notepad, TextEdit or VS Code) and save it as 4pricing-prices.php.

    php4pricing-prices.php
    <?php
    // =====================================================================
    // PASTE YOUR API KEY HERE (panel.4pricing.net → API & keys), between the quotes
    $apiKey = 'PASTE_YOUR_KEY_HERE';
    // =====================================================================
    
    header('Content-Type: text/html; charset=utf-8');
    
    function h($text) { return htmlspecialchars((string) $text, ENT_QUOTES, 'UTF-8'); }
    function money($cents) {
        if ($cents === null) { return '—'; }
        return number_format($cents / 100, 2, '.', ',');
    }
    
    if (strpos($apiKey, 'prc_') !== 0) {
        exit('<p>' . h('The API key is missing: open the file and paste your key on the marked line.') . '</p>');
    }
    
    $ch = curl_init('https://4pricing.net/api/v1/pricing/groups/');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_USERAGENT => 'my-shop-integration/1.0',
        CURLOPT_TIMEOUT => 30,
        CURLOPT_HTTPHEADER => ['X-API-Key: ' . $apiKey, 'Accept: application/json'],
    ]);
    $body = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    
    if ($body === false) {
        exit('<p>' . h('Could not connect to 4pricing:' . ' ' . curl_error($ch)) . '</p>');
    }
    if ($status !== 200) {
        $hints = [401 => 'the API key is wrong or was revoked', 402 => 'the account has no active 4pricing subscription (or not enough credits)', 429 => 'too many requests in one minute, try again in a minute'];
        $hint = isset($hints[$status]) ? ' — ' . $hints[$status] : '';
        exit('<p>' . h('4pricing answered with error' . ' ' . $status . $hint) . '</p><pre>' . h($body) . '</pre>');
    }
    
    $groups = json_decode($body, true);
    $verdicts = [
        'cheapest' => 'You are the cheapest',
        'too_cheap' => 'Much cheaper than the market — you can raise the price',
        'competitive' => 'Competitive',
        'too_expensive' => 'More expensive than most competitors',
        'no_competitors' => 'No competitor price yet',
        'no_own_price' => 'Your price is missing',
    ];
    
    echo '<!doctype html><html><head><meta charset="utf-8"><title>' . h('My monitored products') . '</title>';
    echo '<style>body{font-family:Arial,sans-serif;margin:24px}table{border-collapse:collapse}';
    echo 'th,td{border:1px solid #ddd;padding:6px 10px;text-align:left}th{background:#f5f5f5}</style>';
    echo '</head><body><h1>' . h('My monitored products') . '</h1>';
    
    if (!$groups) {
        echo '<p>' . h('You have no compared products yet.') . '</p>';
    } else {
        echo '<table><tr><th>' . h('Product') . '</th><th>' . h('SKU') . '</th>';
        echo '<th>' . h('Your price') . '</th><th>' . h('Cheapest competitor') . '</th>';
        echo '<th>' . h('Difference') . '</th><th>' . h('Verdict') . '</th></tr>';
        foreach ($groups as $g) {
            $yours = $g['own_price_cents_effective'];
            $cheapest = $g['min_competitor_cents'];
            $diff = '—';
            if ($yours !== null && $cheapest !== null) {
                $diff = ($yours > $cheapest ? '+' : '') . money($yours - $cheapest);
            }
            $verdict = isset($verdicts[$g['verdict']]) ? $verdicts[$g['verdict']] : $g['verdict'];
            echo '<tr><td>' . h($g['name']) . '</td><td>' . h($g['sku']) . '</td>';
            echo '<td>' . money($yours) . '</td><td>' . money($cheapest) . '</td>';
            echo '<td>' . $diff . '</td><td>' . h($verdict) . '</td></tr>';
        }
        echo '</table>';
    }
    echo '</body></html>';
    
    Replace PASTE_YOUR_KEY_HERE with your key, keeping the quotes around it. That is the only line you have to change.
  3. Step 3: Upload the file and open it in your browser

    Upload 4pricing-prices.php to your website with the File Manager or FTP, in the same folder as your site (usually public_html).

    Open https://your-site/4pricing-prices.php in the browser. You will see a table with every compared product: your price, the cheapest competitor price, the difference and the verdict.

The file: list your products with the cheapest competitor price

The file asks 4pricing for your compared products (GET /groups/) and shows them as a table. Reading data is free and does not use credits.

  • “Your price” is the price 4pricing knows for you; “Cheapest competitor” is the lowest price found among the competitors you monitor.
  • Prices are shown in units (12999 cents become 129.99); a dash (—) means 4pricing has no price yet, not zero.
  • If you see an error instead of the table, the message tells you what to fix: wrong key, no active subscription or too many requests in one minute.

Second recipe: send your price

Use this when 4pricing should know your own price without reading it from your shop page — for example straight from your shop's database. Sending your prices is free (no credits) and does not count towards the monitored-URL limit of your plan.

FieldMeaning
skuyour product code; 4pricing finds the product by it, or creates it the first time
namethe product name, as you want to see it in 4pricing
urloptional: the address of the product page in your shop (it is never read)
price_centsthe price multiplied by 100, without decimals: 49.99 → 4999
currencythe currency code, for example EUR, GBP or RON (RON if missing)
php4pricing-send-price.php
<?php
// =====================================================================
// PASTE YOUR API KEY HERE (panel.4pricing.net → API & keys), between the quotes
$apiKey = 'PASTE_YOUR_KEY_HERE';
// =====================================================================

// YOUR PRODUCTS: one block per product. price_cents = price x 100 (49.99 becomes 4999).
$products = [
    [
        'sku' => 'WHITE-TSHIRT-M',
        'name' => 'White T-shirt, size M',
        'url' => 'https://your-shop.example/white-tshirt-m',
        'price_cents' => 4999,
        'currency' => 'EUR',
    ],
];

header('Content-Type: text/html; charset=utf-8');

function h($text) { return htmlspecialchars((string) $text, ENT_QUOTES, 'UTF-8'); }

if (strpos($apiKey, 'prc_') !== 0) {
    exit('<p>' . h('The API key is missing: open the file and paste your key on the marked line.') . '</p>');
}

$ch = curl_init('https://4pricing.net/api/v1/pricing/own-products/bulk');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_USERAGENT => 'my-shop-integration/1.0',
    CURLOPT_TIMEOUT => 60,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => ['X-API-Key: ' . $apiKey, 'Content-Type: application/json', 'Accept: application/json'],
    CURLOPT_POSTFIELDS => json_encode(['items' => $products], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);

if ($body === false) {
    exit('<p>' . h('Could not connect to 4pricing:' . ' ' . curl_error($ch)) . '</p>');
}
if ($status !== 200) {
    $hints = [401 => 'the API key is wrong or was revoked', 402 => 'the account has no active 4pricing subscription (or not enough credits)', 429 => 'too many requests in one minute, try again in a minute'];
    $hint = isset($hints[$status]) ? ' — ' . $hints[$status] : '';
    exit('<p>' . h('4pricing answered with error' . ' ' . $status . $hint) . '</p><pre>' . h($body) . '</pre>');
}

$result = json_decode($body, true);
echo '<h1>' . h('Prices sent to 4pricing') . '</h1>';
echo '<p>' . h('New products created:') . ' ' . (int) $result['created'] . '</p>';
echo '<p>' . h('Products updated:') . ' ' . (int) $result['updated'] . '</p>';
if (!empty($result['errors'])) {
    echo '<p>' . h('Rows with problems:') . '</p><ul>';
    foreach ($result['errors'] as $error) {
        echo '<li>' . h($error) . '</li>';
    }
    echo '</ul>';
}
  • Save it as 4pricing-send-price.php, paste your key, edit the product rows, upload it and open it once: the page shows how many products were created and how many updated.
  • Each request can carry up to 2,000 products; a bad row is reported and the others are still saved. Sending the same sku again updates the price.

The same calls with curl

If you prefer the terminal, these two commands do exactly the same as the two PHP files:

bash
curl https://4pricing.net/api/v1/pricing/groups/ \
  -H "X-API-Key: PASTE_YOUR_KEY_HERE"
bash
curl -X POST https://4pricing.net/api/v1/pricing/own-products/bulk \
  -H "X-API-Key: PASTE_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{"items":[{"sku":"WHITE-TSHIRT-M","name":"White T-shirt, size M","url":"https://your-shop.example/white-tshirt-m","price_cents":4999,"currency":"EUR"}]}'
Keep the key safe
  • The key gives access to your whole 4pricing account. Keep it only in server-side files (PHP, your backend) — never in browser JavaScript or in a public repository.
  • Anyone who knows the address of the uploaded page can see the price table (not the key). Give the file a name that is hard to guess, protect the folder with a password, or delete it when you no longer need it.
  • If a key leaks, revoke it in API & keys — it stops working immediately — and create a new one.

Authentication

  • Every request carries your key in the X-API-Key header. Authorization: Bearer prc_live_… works too (only for keys that start with prc_).
  • There are no scopes: a key can read and change everything in your 4pricing account. Create one key per integration (shop, ERP, report) so you can revoke one without stopping the others.
  • A missing, wrong or revoked key gets 401. A revocation takes effect immediately. Too many invalid keys in one minute get 429.
  • Keys are created, listed and revoked only in the panel (Price Monitoring → API & keys); the API itself cannot manage keys. Creating a key requires an active 4pricing subscription.
http
X-API-Key: prc_live_…
# or
Authorization: Bearer prc_live_…

Base URL and conventions

  • Every path in this documentation is relative to the base URL — /groups/ means https://4pricing.net/api/v1/pricing/groups/.
  • Requests and responses are JSON (Content-Type: application/json).
  • Amounts are whole numbers in minor units (cents): price_cents = 12999 means 129.99. Percentages are in basis points (100 = 1%).
  • An unknown value is null, never 0 — show it as “—”.
  • Collections are called with a trailing slash (/products/, /groups/, /insights/).
  • Paginated lists return the total in the X-Total-Count header (for example GET /products/?skip=0&limit=200).
  • Send a User-Agent header that names your integration (for example my-shop-integration/1.0). The firewall in front of the API answers 403 to some generic library defaults, such as Python-urllib.

Rate limit, subscription and plan limits

Rate limit

  • 60 requests per minute per ACCOUNT, in a sliding window — all the keys of the same account share the same 60.
  • Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (the window length, in seconds).
  • Over the limit you get 429 with a Retry-After header (seconds). Wait that long, then retry — the samples below do it for you.
http
HTTP/2 200
x-ratelimit-limit: 60
x-ratelimit-remaining: 59
x-ratelimit-reset: 60

HTTP/2 429
retry-after: 60
{"detail":"Rate limit exceeded"}

Subscription and plan limits

  • The API needs an active 4pricing subscription; without one every request gets 402 with code plan_required.
  • Every plan includes the API with the same limits as the panel. Going over a limit when you add or reactivate something gets 402 with code plan_limit and a message that says which limit.
LimitMeaning
Monitored URLsEvery product-page address 4pricing reads counts separately: your own page and each competitor page. Prices you send with /own-products do not count.
Competitor shopsThe number of distinct competitor websites (domains) you monitor.
Minimum check intervalHow often the same URL may be checked. Shorter intervals are raised to your plan's minimum automatically; refreshing a URL too early gets 402 plan_limit.

Which calls use credits

Reading a shop page costs credits from your account's wallet. The calls below check the balance BEFORE reading anything: if it is not enough you get 402 insufficient_credits, and if the wallet cannot be read right now you get 503 credit_wallet_unavailable (retry later). Nothing is read and nothing is charged in either case.

Calls that use credits

MethodEndpointWhat it doesCost
POST/groups/{group_id}/membersAdd a URL to the group, by product_id or from a new address (a new address is read immediately)Credits
POST/groups/{group_id}/refreshCheck every URL of the group nowCredits
POST/groups/{group_id}/suggestions“Find competitors”: proposed competitor URLs with a score and the reasonCredits
POST/products/Start monitoring a URL (url, name, role own or competitor) — it is read immediatelyCredits
POST/products/{product_id}/refreshCheck this URL's price now (respecting your plan's minimum interval)Credits
POST/search/Search a product's live price at retailers (credits for each retailer searched)Credits

Free calls

  • Every read (GET): products, groups, comparisons, history, alerts, insights, briefings, credits, stats.
  • POST /own-products/ and POST /own-products/bulk — your own prices.
  • POST /sales/bulk — your sales.
  • Alerts, settings, confirmations and everything else that does not read a shop page.
  • The scheduled monitoring of the URLs you track also uses credits, at every check; GET /credits/ shows your balance and the forecast.
  • POST /products/import adds many URLs at once without reading them immediately: they are checked, and charged, by the scheduled monitoring.

Endpoint reference

Every operation a key can reach, grouped by what it is for. The OpenAPI specification describes each one field by field, with the exact request and response bodies.

Your own prices

Tell 4pricing your price directly, without reading your shop page. Free.

MethodEndpointWhat it doesCost
POST/own-products/Send your current price for one product (found by sku, else by name; created if missing)Free
POST/own-products/bulkSend your prices for up to 2,000 products in one request; bad rows are reported, the rest are savedFree

Compared products (groups)

A group is one product of yours together with the competitor pages that sell the same thing. It holds the verdict and the recommended price.

MethodEndpointWhat it doesCost
GET/groups/All compared products, each with your price, the cheapest competitor price, the verdict and the recommended priceFree
POST/groups/Create a compared product (name, sku, your price, cost, pricing strategy)Free
GET/groups/{group_id}One compared productFree
PUT/groups/{group_id}Update price, cost, strategy, check interval or the shop link (store_kind)Free
DELETE/groups/{group_id}Delete the group; its URLs stay monitoredFree
GET/groups/{group_id}/comparisonYour position against every competitor, with the recommended price and how it was computedFree
GET/groups/{group_id}/historyPrice history of every URL in the groupFree
GET/groups/seriesYour market position, day by day, across all productsFree
POST/groups/{group_id}/membersAdd a URL to the group, by product_id or from a new address (a new address is read immediately)Credits
DELETE/groups/{group_id}/members/{product_id}Take a URL out of the group (it stays monitored)Free
POST/groups/{group_id}/refreshCheck every URL of the group nowCredits
POST/groups/{group_id}/apply-recommendedMake the recommended price your own price in 4pricing (send the price you saw; 409 if it moved)Free
GET/groups/suggestions/sourcesSources available for “Find competitors”Free
POST/groups/{group_id}/suggestions“Find competitors”: proposed competitor URLs with a score and the reasonCredits

Monitored URLs

One row per page address 4pricing reads, yours or a competitor's (role own or competitor).

MethodEndpointWhat it doesCost
GET/products/Monitored URLs, paginated (skip, limit up to 500; total in X-Total-Count)Free
POST/products/Start monitoring a URL (url, name, role own or competitor) — it is read immediatelyCredits
POST/products/importAdd many URLs at once; they are read later by the scheduled monitoringFree
GET/products/exportCSV export of the active URLsFree
GET/products/{product_id}One monitored URL with its latest priceFree
PATCH/products/{product_id}Change the name, category or check intervalFree
DELETE/products/{product_id}Stop monitoring a URL (it is kept as inactive)Free
POST/products/{product_id}/refreshCheck this URL's price now (respecting your plan's minimum interval)Credits
GET/products/{product_id}/historyPrice history of one URLFree
GET/products/sparklinesA short price history for several URLs at onceFree
GET/products/price-changesURLs whose price changed at the last checkFree

Look up a product's price right now at retailers.

MethodEndpointWhat it doesCost
POST/search/Search a product's live price at retailers (credits for each retailer searched)Credits

Alerts

Price rules and the inbox of alerts that went off.

MethodEndpointWhat it doesCost
GET/alerts/Your price alertsFree
POST/alerts/Create an alert (price below/above, drop in %, back in stock, undercut by a competitor…)Free
DELETE/alerts/{alert_id}Delete an alertFree
POST/alerts/{alert_id}/rearmRe-arm an alert that went offFree
GET/alerts/eventsInbox of alerts that went offFree
GET/alerts/events/unread-countHow many unread alerts you haveFree
POST/alerts/events/read-allMark the alerts as read (optionally only those before a moment)Free
POST/alerts/events/{event_id}/readMark one alert as readFree

Credits

Balance, forecast and every movement of credits.

MethodEndpointWhat it doesCost
GET/credits/Credit balance and consumption forecastFree
GET/credits/ledgerEvery credit movementFree
GET/credits/by-productCredits used per URL (total and last 30 days)Free

Your sales

Daily sales per product, used to compute the value of each opportunity. Free.

MethodEndpointWhat it doesCost
POST/sales/bulkDaily sales per product (sku or group_id, day, units_sold, revenue_cents; optional stock_units, cost_cents), up to 2,000 rows; sending the same product and day again overwrites itFree

Insights, opportunities and briefings

What changed, why it matters and what to do — plus the daily briefing.

MethodEndpointWhat it doesCost
GET/insights/Insights with evidence and price proposals (X-Total-Count)Free
GET/insights/{insight_id}One insight in detailFree
POST/insights/{insight_id}/dismissDismiss an insightFree
GET/opportunities/Summary of opportunities: value from your sales, kept separate from the market estimateFree
GET/briefings/History of daily briefingsFree
GET/briefings/latestThe latest daily briefingFree
POST/briefings/generateRebuild today's briefing from the open insights (it sends nothing)Free

Price actions

Price changes that were proposed, sent, applied or reverted. Through the API you can read them and confirm those received by webhook.

MethodEndpointWhat it doesCost
GET/actions/Price actions: proposed, sent, applied, revertedFree
POST/actions/{action_id}/confirmConfirm that your system applied a price received through price.apply or price.revertFree

Settings

Account settings, including the webhook address and secret.

MethodEndpointWhat it doesCost
GET/settings/Account settings (retailers, repricing strategy, alert delivery, webhook)Free
PUT/settings/Change settings, including alert_webhook_url, alert_webhook_secret and alert_webhook_enabledFree

Statistics

Account overview and the health of price checks.

MethodEndpointWhat it doesCost
GET/stats/Account statistics: URLs, alerts, 24-hour changes, groupsFree
GET/stats/scrapersHealth of price checks per retailerFree

53 endpoints · https://4pricing.net/api/v1/pricing/openapi.json

What the API cannot do

Some things stay in the panel on purpose, where a signed-in person decides. Through the API these paths answer 404:

PathWhy
/keys/…Creating and revoking keys — a key cannot create more keys.
/autopilot/…The autopilot policy, which decides what may change without a person; a leaked key must not be able to widen it.
/watchlists/…Watchlists, which spend credits on every run.
/actions/{action_id}/approve · reject · revertApproving, rejecting or reverting a price change is a decision a person makes in the panel. The API can only read actions and confirm them.
/insights/{insight_id}/act · /insights/runActing on an insight (it writes prices) and recomputing insights.
/admin/… · /internal/…Internal administration of the platform.

Webhooks

4pricing can send events to an address of yours (a webhook): it makes a JSON POST every time something happens, so your system does not have to keep asking.

There is one webhook address per account; every event type goes to it. Tell them apart by the event field — the alert is the only event without one.

Turning it on

  • Set alert_webhook_url (a public http/https address on port 80 or 443), alert_webhook_secret and alert_webhook_enabled: true — in the panel or with PUT /settings/.
  • An address that resolves to a private or internal network is refused with 422. Redirects are not followed.
  • For briefing.ready also turn on briefing_webhook_enabled. price.apply and price.revert are sent only for groups linked to your shop with store_kind = webhook, and only if a secret is set.
bash
curl -X PUT https://4pricing.net/api/v1/pricing/settings/ \
  -H "X-API-Key: PASTE_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{"alert_webhook_url":"https://your-shop.example/4pricing-webhook.php","alert_webhook_secret":"YOUR_WEBHOOK_SECRET","alert_webhook_enabled":true,"briefing_webhook_enabled":true}'

Events

EventMeaning
alertA price alert went off. Body: event_id (deduplicate on it), alert_id, alert_type, message, message_code, message_params, price_cents, currency, triggered_at, product, group, panel_url. No event field.
price.applyAn approved price to apply in your shop: action_id, group (id, name, sku, ean, store_product_id), old_price_cents, new_price_cents, currency, reason_code, mode and confirm.path.
price.revertGo back to the previous price. Same body as price.apply; apply and confirm it the same way.
briefing.readyThe daily briefing is ready: day, headline, summary, sections, total_value_cents (only from your sales), currency, panel_url.

Headers of every webhook request

HeaderMeaning
X-4Pricing-TimestampWhen the request was sent, in Unix seconds. Reject requests that are too old (for example over 5 minutes).
X-4Pricing-Signaturesha256= followed by the hex HMAC-SHA256 of the text “<timestamp>.<raw body>”, computed with your webhook secret. Present when a secret is set (always on price.apply and price.revert).
Content-Typeapplication/json; charset=utf-8
User-Agent4pricing-alerts/1.0

Verifying the signature

Recompute the signature from the timestamp header and the RAW body you received, compare it in constant time and check the timestamp. Only then process the event. Answer with any 2xx when you have accepted it.

php
<?php
$secret = 'YOUR_WEBHOOK_SECRET';

// Use the RAW body, exactly as received — not a re-encoded JSON
$body = file_get_contents('php://input');
$timestamp = isset($_SERVER['HTTP_X_4PRICING_TIMESTAMP']) ? $_SERVER['HTTP_X_4PRICING_TIMESTAMP'] : '';
$signature = isset($_SERVER['HTTP_X_4PRICING_SIGNATURE']) ? $_SERVER['HTTP_X_4PRICING_SIGNATURE'] : '';
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);

// Wrong signature or older than 5 minutes: reject
if (!hash_equals($expected, $signature) || abs(time() - (int) $timestamp) > 300) {
    http_response_code(401);
    exit;
}

$event = json_decode($body, true);
if (isset($event['event']) && ($event['event'] === 'price.apply' || $event['event'] === 'price.revert')) {
    // Apply new_price_cents to the product in your shop (match it by sku or store_product_id)
    // $event['group']['sku'], $event['new_price_cents'], $event['currency']
    // Then confirm: POST /actions/{action_id}/confirm with your API key
}
http_response_code(200);
echo 'ok';
javascript
// Node.js, no package needed
const http = require("http");
const crypto = require("crypto");
const SECRET = process.env.WEBHOOK_SECRET; // YOUR_WEBHOOK_SECRET

http.createServer((req, res) => {
  const chunks = [];
  req.on("data", (c) => chunks.push(c));
  req.on("end", () => {
    // Use the RAW body, exactly as received — not a re-encoded JSON
    const body = Buffer.concat(chunks);
    const ts = String(req.headers["x-4pricing-timestamp"] || "");
    const sig = String(req.headers["x-4pricing-signature"] || "");
    const expected = "sha256=" + crypto.createHmac("sha256", SECRET).update(ts + ".").update(body).digest("hex");
    const fresh = Math.abs(Date.now() / 1000 - Number(ts)) <= 300;
    const same = sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
    if (!same || !fresh) {
      // Wrong signature or older than 5 minutes: reject
      res.writeHead(401);
      return res.end();
    }
    const event = JSON.parse(body.toString("utf8"));
    if (event.event === "price.apply" || event.event === "price.revert") {
      // Apply new_price_cents to the product in your shop (match it by sku or store_product_id)
      // Then confirm: POST /actions/{action_id}/confirm with your API key
    }
    res.writeHead(200);
    res.end("ok");
  });
}).listen(3000);
python
# Python 3, standard library only
import hashlib
import hmac
import time

SECRET = "YOUR_WEBHOOK_SECRET"


def is_valid(raw_body: bytes, timestamp: str, signature: str) -> bool:
    # Use the RAW body, exactly as received — not a re-encoded JSON
    if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
        return False
    expected = "sha256=" + hmac.new(
        SECRET.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

Delivery and retries

  • Alerts: on a connection or DNS error, 429 or 5xx, the request is retried with exponential backoff, at most 5 attempts. Any other 4xx is final.
  • If the request left but no answer came within 10 seconds it is not sent again (to avoid a duplicate) — you may still rarely get a duplicate, so deduplicate on event_id.
  • price.apply and price.revert are sent once: if delivery fails the action becomes failed. Deduplicate on action_id.
  • briefing.ready is sent once a day.

Confirming a price you applied

  • A price.apply is not considered applied until your system says so. After you change the price in your shop, call POST /actions/{action_id}/confirm with your API key: the action moves from “sent” to “applied”.
  • Confirming twice is harmless — an action that is already applied is returned as it is.
bash
curl -X POST https://4pricing.net/api/v1/pricing/actions/ACTION_ID/confirm \
  -H "X-API-Key: PASTE_YOUR_KEY_HERE"

Example bodies

json · price.apply
{
  "event": "price.apply",
  "action_id": "8d0f2c4e-0b7a-4f7e-9d61-3c2b1a0e9f55",
  "tenant_id": "…",
  "group": {
    "id": "2fa912df-440c-4ddf-896a-5d558d56085f",
    "name": "Canon RF 50mm F1.8 STM",
    "sku": "4515C005AA",
    "ean": "4549292181623",
    "store_product_id": "1042"
  },
  "old_price_cents": 124900,
  "new_price_cents": 118699,
  "currency": "RON",
  "reason_code": "undercut",
  "mode": "manual",
  "confirm": { "method": "POST", "path": "/api/v1/pricing/actions/8d0f2c4e-0b7a-4f7e-9d61-3c2b1a0e9f55/confirm" }
}
json · alert
{
  "event_id": 18342,
  "alert_id": "c1b5…",
  "alert_type": "competitor_undercut",
  "tenant_id": "…",
  "message_code": "competitor_undercut",
  "message": "…",
  "message_params": { "…": "…" },
  "price_cents": 118990,
  "currency": "RON",
  "triggered_at": "2026-10-13T07:05:12",
  "product": { "id": "…", "name": "…", "url": "https://…", "retailer": "…", "current_price_cents": 118990, "in_stock": true },
  "group": { "id": "…", "name": "Canon RF 50mm F1.8 STM", "sku": "4515C005AA" },
  "panel_url": "https://panel.4pricing.net/…"
}

Errors

Errors come with the right HTTP status and a JSON body {"detail": …}. detail is a text, an object with a code you can handle in your program, or — on 422 — the list of invalid fields.

StatusCodeMeaning
401—The API key is missing, wrong or revoked.
402plan_requiredThe account has no active 4pricing subscription.
402plan_limitA plan limit would be exceeded (monitored URLs, competitor shops) or a refresh came before the minimum interval. The message says which.
402insufficient_creditsNot enough credits for a call that reads a shop page. Nothing was read or charged.
403—Blocked by the firewall before reaching the API — usually a generic User-Agent such as Python-urllib. Send your own User-Agent.
404—The resource does not exist, does not belong to your account, or the path is not available through the API.
409recommendation_moved · own_from_listing · invalid_state · webhook_secret_requiredConflict: the recommended price moved (recommendation_moved), the action is in another state (invalid_state), a webhook secret is required, or the operation does not apply to this item.
422—Invalid data, for example a URL without http/https or a missing required field. detail lists the fields.
429—More than 60 requests per minute for the account (or too many invalid keys). Wait for Retry-After seconds.
503credit_wallet_unavailableThe credit wallet cannot be read right now. Retry later; nothing was charged.

Error bodies

json
{"detail": "Invalid or missing API key"}
{"detail": {"code": "plan_required", "message": "…"}}
{"detail": {"code": "insufficient_credits", "message": "…"}}
{"detail": [{"loc": ["body", "price_cents"], "msg": "…", "type": "…"}]}

Code samples

The same read in four languages, with a pause-and-retry on 429, then sending sales. Replace PASTE_YOUR_KEY_HERE with your key (or set the PRICING_API_KEY environment variable).

Node.js: read your compared products

javascript
// Node.js 18+ (fetch is built in, no package needed)
const API_KEY = process.env.PRICING_API_KEY; // PASTE_YOUR_KEY_HERE
const BASE = "https://4pricing.net/api/v1/pricing";

async function call(path, options = {}) {
  for (let attempt = 0; attempt < 3; attempt++) {
    const res = await fetch(BASE + path, {
      ...options,
      headers: { "X-API-Key": API_KEY, "Content-Type": "application/json", ...(options.headers || {}) },
    });
    if (res.status === 429) {
      // Over 60 requests/minute: wait for Retry-After seconds, then try again
      const wait = Number(res.headers.get("Retry-After") || 60);
      await new Promise((r) => setTimeout(r, wait * 1000));
      continue;
    }
    if (!res.ok) throw new Error(res.status + " " + (await res.text()));
    return res.status === 204 ? null : res.json();
  }
  throw new Error("rate limited");
}

async function main() {
  // Your compared products: your price, the cheapest competitor, the verdict
  const groups = await call("/groups/");
  for (const g of groups) {
    console.log(g.name, g.own_price_cents_effective, g.min_competitor_cents, g.verdict);
  }
}

main().catch((err) => {
  console.error(err.message);
  process.exit(1);
});

Python: read your compared products

python
# Python 3, standard library only
import json
import os
import time
import urllib.error
import urllib.request

API_KEY = os.environ["PRICING_API_KEY"]  # PASTE_YOUR_KEY_HERE
BASE = "https://4pricing.net/api/v1/pricing"


def call(path, method="GET", payload=None):
    data = json.dumps(payload).encode() if payload is not None else None
    for _ in range(3):
        req = urllib.request.Request(BASE + path, data=data, method=method, headers={
            "X-API-Key": API_KEY, "Content-Type": "application/json", "Accept": "application/json",
            # Set your own User-Agent: the default one of some HTTP libraries (Python urllib) is blocked with 403
            "User-Agent": "my-shop-integration/1.0",
        })
        try:
            with urllib.request.urlopen(req, timeout=30) as res:
                body = res.read()
                return json.loads(body) if body else None
        except urllib.error.HTTPError as err:
            if err.code == 429:
                # Over 60 requests/minute: wait for Retry-After seconds, then try again
                time.sleep(int(err.headers.get("Retry-After", "60")))
                continue
            raise
    raise RuntimeError("rate limited")


# Your compared products: your price, the cheapest competitor, the verdict
for g in call("/groups/"):
    print(g["name"], g["own_price_cents_effective"], g["min_competitor_cents"], g["verdict"])

curl: send your daily sales

bash
curl -X POST https://4pricing.net/api/v1/pricing/sales/bulk \
  -H "X-API-Key: PASTE_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{"rows":[{"sku":"WHITE-TSHIRT-M","day":"2026-10-12","units_sold":7,"revenue_cents":34993}]}'

PHP: send your daily sales

php
<?php
$apiKey = 'PASTE_YOUR_KEY_HERE';

// Daily sales per product (by sku or group_id), up to 2,000 rows per request
$rows = [
    ['sku' => 'WHITE-TSHIRT-M', 'day' => '2026-10-12', 'units_sold' => 7, 'revenue_cents' => 34993],
];

$ch = curl_init('https://4pricing.net/api/v1/pricing/sales/bulk');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_USERAGENT => 'my-shop-integration/1.0',
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => ['X-API-Key: ' . $apiKey, 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode(['rows' => $rows]),
]);
$body = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE) . ' ' . $body;   // 200 {"upserted": 1, "skipped": []}

Frequently asked questions

Do I need to be a programmer to use the 4pricing API?

No. The quick start gives you a ready-made PHP file: you create a key in the panel, paste it into the file, upload the file to your hosting and open it in the browser. For deeper integrations (an ERP, a shop platform) a developer can use the endpoint reference and the OpenAPI specification.

How much does the 4pricing API cost?

The API is included in every 4pricing subscription, with the same limits as the panel. Reading your data is free; only the calls that read a shop page (adding a new URL, refreshing, live search, finding competitors) use credits, like in the panel.

Which calls use credits?

POST /products/, POST /products/{product_id}/refresh, POST /groups/{group_id}/refresh, POST /groups/{group_id}/members with a new URL, POST /groups/{group_id}/suggestions and POST /search/. If the balance is not enough you get 402 insufficient_credits and nothing is read.

Do the prices I send through the API count towards my plan's URL limit?

No. Only addresses 4pricing reads from a page count — your own product pages and each competitor page separately. Prices sent with POST /own-products/ or /own-products/bulk count neither as URLs nor as credits.

Can I limit what a key can access?

No, there are no scopes: a key sees and can change your whole 4pricing account. Use a separate key for every integration and revoke any key you no longer use; a revocation takes effect immediately.

What happens if I send more than 60 requests per minute?

You get 429 with a Retry-After header. The limit is per account, shared by all your keys. Wait the number of seconds in Retry-After and try again; for many products use the bulk calls (up to 2,000 rows per request).

How do I get the recommended price into my shop automatically?

Link the group to your shop with store_kind = webhook, set a webhook address and secret, and handle the price.apply event: change the price in your shop, then call POST /actions/{action_id}/confirm. Approving a price change stays a decision made in the panel.

Is there an SDK or a ready-made plugin?

There is no official SDK or plugin. The API is plain REST with JSON, so any language that can make HTTPS requests works; the OpenAPI specification can be loaded into tools that generate a client automatically.