Skip to content

GetPrice API reference

API reference#

Look up competitor prices for your own SKUs and feed them into your ERP, BI or pricing tools. You get the same prices you see in the GetPrice dashboard.

Authentication#

Send your API key in the Authorization header: Authorization: Bearer gp_live_…. Never put the key in a URL. An admin creates your organisation's one key in Settings → API & Integrations; it is shown once, and the same page regenerates it. Scopes are listed in GET /v1/me.

ScopeGrants
catalog:readyour products, competitors, SKU links
matches:readcomparison sets (competitor matches and prices)
skus:writecreate, replace and remove SKU links
BASE=https://api.getprice.ai/v1
curl "$BASE/me" \
  -H "Authorization: Bearer $GETPRICE_API_KEY"
import os

import requests

BASE = "https://api.getprice.ai/v1"
KEY = os.environ["GETPRICE_API_KEY"]

resp = requests.get(
    f"{BASE}/me",
    headers={"Authorization": f"Bearer {KEY}"},
    timeout=30,
)
resp.raise_for_status()
me = resp.json()
print(me["company"]["name"], me["limits"]["per_minute"])
interface Me {
  company: { name: string };
  limits: { per_minute: number };
}

const BASE = "https://api.getprice.ai/v1";
const KEY = process.env.GETPRICE_API_KEY;
const url = `${BASE}/me`;
const res = await fetch(url, {
  headers: { Authorization: `Bearer ${KEY}` },
});
if (!res.ok) {
  throw new Error(`GetPrice returned ${res.status}`);
}
const me = (await res.json()) as Me;
console.log(me.company.name, me.limits.per_minute);
<?php
$base = "https://api.getprice.ai/v1";
$key = getenv("GETPRICE_API_KEY");
$ch = curl_init("$base/me");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer $key",
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($body === false || $status >= 400) {
    throw new RuntimeException(
        "GetPrice returned $status: $body"
    );
}
$data = json_decode(
    $body, true, flags: JSON_THROW_ON_ERROR
);

echo $data["company"]["name"], " ",
    $data["limits"]["per_minute"], "\n";
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using Naming = System.Text.Json.JsonNamingPolicy;

var key = Environment.GetEnvironmentVariable(
    "GETPRICE_API_KEY");
var api = new Uri("https://api.getprice.ai/v1/");
var http = new HttpClient { BaseAddress = api };
http.Timeout = TimeSpan.FromSeconds(30);
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", key);
var json = new JsonSerializerOptions
{
    PropertyNamingPolicy = Naming.SnakeCaseLower,
};

var res = await http.GetAsync("me");
res.EnsureSuccessStatusCode();
var me = await res.Content
    .ReadFromJsonAsync<Me>(json);
Console.WriteLine(
    $"{me!.Company.Name} {me.Limits.PerMinute}");

record Me(Company Company, Limits Limits);
record Company(string Name);
record Limits(int PerMinute);
import requests
from odoo import models

API = "https://api.getprice.ai/v1"


class ResConfigSettings(models.TransientModel):
    _inherit = "res.config.settings"

    def action_getprice_test_key(self):
        """Settings button: check the GetPrice key."""
        icp = self.env["ir.config_parameter"].sudo()
        key = icp.get_param("getprice.api_key")
        resp = requests.get(
            f"{API}/me",
            headers={"Authorization": f"Bearer {key}"},
            timeout=30,
        )
        resp.raise_for_status()
        name = resp.json()["company"]["name"]
        return {
            "type": "ir.actions.client",
            "tag": "display_notification",
            "params": {
                "message": f"Connected as {name}",
                "type": "success",
            },
        }

Errors#

Errors use RFC 9457 problem details with Content-Type: application/problem+json. Branch on code, never on title or detail, and quote request_id when you contact support.

Error response
401
{
  "type": "https://api.getprice.ai/errors/unauthenticated",
  "title": "Authentication required",
  "status": 401,
  "detail": "Send your API key as 'Authorization: Bearer gp_live_…'.",
  "code": "unauthenticated",
  "request_id": "req_01JBX4Q8ZJ7M2K"
}

HTTP status codes

StatusMeaningWhat to do
200, 201, 204

It worked

Read the body, if there is one

304

Nothing changed since your ETag

Use your cached copy

400

The request is malformed

Fix the request; check code and errors

401

No key, or a key that doesn't work

Check the Authorization header and the key

403

The key can't do this

Use a key with the right scope

404

Not found for your company

Check the id or SKU

405

Wrong HTTP method

See the Allow header

412

A precondition failed

Read the resource again, then decide

413

The body is over 1 MB, whether or not you sent Content-Length

Send less

415

The body isn't JSON

Send Content-Type: application/json

422

The JSON body is invalid

Fix the field named in errors

429

Too many requests

Wait Retry-After seconds

500

Something failed on our side

Don't retry; contact support with the request_id

503

Temporarily unavailable

Wait Retry-After seconds, then retry

A 401 carries a WWW-Authenticate: Bearer realm="api.getprice.ai" header.

Attributes

  • type#string

    Documentation URL for this error; code is its slug.

  • title#string
  • status#integer
  • detail#string

    Human-readable explanation for this occurrence.

  • code#string

    Stable machine-readable error code. Open set: new codes may be added, so handle an unknown code by its HTTP status. key_revoked, plan_required, conflict, cursor_expired, idempotency_key_reused, precondition_required, concurrency_limited and quota_exceeded are reserved for later features.

    See Error codes

  • request_id#string
    Example
    req_01JBX4Q8ZJ7M2K
  • errors#array of objects

    Field-level details for validation errors.

    Show child attributesHide child attributes
    • pointer#string

      JSON Pointer into the request body, e.g. /product_id.

    • parameter#string

      Name of the offending query parameter.

    • detail#string
  • limit#string

    On 429, which limit was hit (e.g. sustained).

  • retry_after#integer

    On 429, seconds to wait (same as the Retry-After header).

Error codes

CodeStatusMeaningHow to fix
validation_failed400A parameter value is invalid, such as more than 100 sku values or limit=0Fix the value named in errors. Don't retry.
unknown_parameter400The endpoint doesn't take this query parameter, often because of a typoFix the parameter name. Don't retry.
invalid_parameter400A parameter value contains a control characterRemove the character. Don't retry.
cursor_mismatch400The filters or the endpoint changed while you paged with a cursorStart again without a cursor. Don't retry.
invalid_cursor400The cursor is malformed, altered or expiredStart again without a cursor. Don't retry.
unauthenticated401There's no Authorization: Bearer headerSend your key. Don't retry.
invalid_key401The key is malformed, unknown, revoked or expired. We don't say whichCheck for copy-paste damage. If the key is intact, an admin can regenerate it in Settings. Don't retry.
insufficient_scope403The key lacks this endpoint's scopeUse a key with that scope. Don't retry.
not_found404There's no such resource for your companyCheck the id or SKU. Don't retry.
product_not_found404product_id isn't one of your own productsUse an id from GET /v1/products. Don't retry.
method_not_allowed405The path doesn't support this HTTP methodUse a method from the Allow header. Don't retry.
precondition_failed412You sent If-None-Match: * and the SKU is already linked, or If-Match didn't matchRead the link again, then decide. Don't retry.
payload_too_large413The request body is over 1 MB, whether or not you sent Content-LengthSend a smaller body. Don't retry.
unsupported_media_type415The body isn't application/jsonSet Content-Type: application/json. Don't retry.
validation_failed422The JSON body is invalid, such as a missing product_idFix the field named in errors. Don't retry.
rate_limited429This request costs more units than you have left this minuteWait Retry-After seconds, then retry
internal_error500Something failed on our sideContact support with the request_id. Don't retry.
unavailable503GetPrice can't guarantee correct match data right now, or is too busyWait Retry-After seconds, then retry

Codes reserved for features after the pilot are listed under Reserved codes.

Retrieve your API key#

Returns the API key you're calling with: its name and prefix, your company, its scopes and its rate limit. Any valid key can call it, whatever its scopes. Use it to check that your key works.

BASE=https://api.getprice.ai/v1
curl "$BASE/me" \
  -H "Authorization: Bearer $GETPRICE_API_KEY"
import os

import requests

BASE = "https://api.getprice.ai/v1"
KEY = os.environ["GETPRICE_API_KEY"]

resp = requests.get(
    f"{BASE}/me",
    headers={"Authorization": f"Bearer {KEY}"},
    timeout=30,
)
resp.raise_for_status()
me = resp.json()
print(me["company"]["name"], me["limits"]["per_minute"])
interface Me {
  company: { name: string };
  limits: { per_minute: number };
}

const BASE = "https://api.getprice.ai/v1";
const KEY = process.env.GETPRICE_API_KEY;
const url = `${BASE}/me`;
const res = await fetch(url, {
  headers: { Authorization: `Bearer ${KEY}` },
});
if (!res.ok) {
  throw new Error(`GetPrice returned ${res.status}`);
}
const me = (await res.json()) as Me;
console.log(me.company.name, me.limits.per_minute);
<?php
$base = "https://api.getprice.ai/v1";
$key = getenv("GETPRICE_API_KEY");
$ch = curl_init("$base/me");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer $key",
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($body === false || $status >= 400) {
    throw new RuntimeException(
        "GetPrice returned $status: $body"
    );
}
$data = json_decode(
    $body, true, flags: JSON_THROW_ON_ERROR
);

echo $data["company"]["name"], " ",
    $data["limits"]["per_minute"], "\n";
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using Naming = System.Text.Json.JsonNamingPolicy;

var key = Environment.GetEnvironmentVariable(
    "GETPRICE_API_KEY");
var api = new Uri("https://api.getprice.ai/v1/");
var http = new HttpClient { BaseAddress = api };
http.Timeout = TimeSpan.FromSeconds(30);
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", key);
var json = new JsonSerializerOptions
{
    PropertyNamingPolicy = Naming.SnakeCaseLower,
};

var res = await http.GetAsync("me");
res.EnsureSuccessStatusCode();
var me = await res.Content
    .ReadFromJsonAsync<Me>(json);
Console.WriteLine(
    $"{me!.Company.Name} {me.Limits.PerMinute}");

record Me(Company Company, Limits Limits);
record Company(string Name);
record Limits(int PerMinute);
import requests
from odoo import models

API = "https://api.getprice.ai/v1"


class ResConfigSettings(models.TransientModel):
    _inherit = "res.config.settings"

    def action_getprice_test_key(self):
        """Settings button: check the GetPrice key."""
        icp = self.env["ir.config_parameter"].sudo()
        key = icp.get_param("getprice.api_key")
        resp = requests.get(
            f"{API}/me",
            headers={"Authorization": f"Bearer {key}"},
            timeout=30,
        )
        resp.raise_for_status()
        name = resp.json()["company"]["name"]
        return {
            "type": "ir.actions.client",
            "tag": "display_notification",
            "params": {
                "message": f"Connected as {name}",
                "type": "success",
            },
        }
Response
200
{
  "key": {
    "name": "Odoo production",
    "prefix": "k3Xa91Qz",
    "environment": "live"
  },
  "company": {
    "name": "Example Wholesale (Pty) Ltd"
  },
  "scopes": [
    "catalog:read",
    "matches:read",
    "skus:write"
  ],
  "limits": {
    "per_minute": 120,
    "window_seconds": 60
  }
}

Headers

  • If-None-Match#string

    An ETag from a previous response. If nothing changed you get 304 Not Modified with no body.

Returns

200

The calling key.

  • key#object
    Show child attributesHide child attributes
    • name#string

      The key's name as shown in Settings → API & Integrations.

    • prefix#string

      The public part of the key. Safe to quote to support; identifies the key in logs.

    • environment#string

      live for production keys. Open set.

      One of (more may be added)
      live, test
  • company#object
    Show child attributesHide child attributes
    • name#string
  • scopes#array of strings
  • limits#object
    Show child attributesHide child attributes
    • per_minute#integer

      Rate-limit units per window. Most requests cost 1 unit.

    • window_seconds#integer

      Length of the rate-limit window in seconds.

Errors

  • 400

    The request is malformed: an invalid or unknown query parameter, a bad cursor, more than 100 sku values, or an invalid SKU in the path (empty after trimming or over 100 characters).

  • 401

    No key (unauthenticated), or a key that is malformed, unknown, revoked or expired (invalid_key; for security the response doesn't say which).

  • 429

    This request costs more rate-limit units than you have left.

  • 500

    Something went wrong on our side.

  • 503

    Temporarily unavailable: GetPrice can't guarantee correct match data right now, or the database is saturated.

How errors are shaped

The comparison set object#

The comparison set object
{
  "id": "set_x7Kp2Lm9",
  "name": "Baked Beans in Tomato Sauce 410g",
  "sku": "10072L",
  "own_product": {
    "id": "prd_8fK2mQa1",
    "name": "Baked Beans in Tomato Sauce 410g",
    "url": "https://shop.example.com/product/10072L",
    "price": {
      "amount": "18.99",
      "was": null,
      "currency": "ZAR"
    },
    "last_collected_at": "2026-11-02T04:12:09Z",
    "stale": false
  },
  "competitors": [
    {
      "competitor": {
        "id": "cmp_3LqV8nP2",
        "name": "Harbour Market",
        "domain": "harbourmarket.example.com"
      },
      "product": {
        "id": "prd_Q2vN9cD4",
        "name": "Baked Beans 410g",
        "url": "https://harbourmarket.example.com/baked-beans-in-tomato-sauce-410g"
      },
      "price": {
        "amount": "17.99",
        "was": "19.99",
        "currency": "ZAR"
      },
      "last_collected_at": "2026-11-02T03:05:11Z",
      "availability": "unknown",
      "status": "active",
      "stale": false,
      "match": {
        "review": "same",
        "reviewed_by": "customer",
        "confidence": null
      }
    },
    {
      "competitor": {
        "id": "cmp_7RwK4tB9",
        "name": "Cape Fresh",
        "domain": "capefresh.example.com"
      },
      "product": {
        "id": "prd_Hm3Wc8Kp",
        "name": "Baked Beans In Tomato Sauce 410g",
        "url": "https://capefresh.example.com/baked-beans-in-tomato-sauce-410g"
      },
      "price": {
        "amount": "18.99",
        "was": null,
        "currency": "ZAR"
      },
      "last_collected_at": "2026-11-02T03:11:40Z",
      "availability": "unknown",
      "status": "active",
      "stale": false,
      "match": {
        "review": "same",
        "reviewed_by": "getprice",
        "confidence": 0.94
      }
    },
    {
      "competitor": {
        "id": "cmp_9TzM2cX5",
        "name": "Northwind Wholesale",
        "domain": "northwindwholesale.example.com"
      },
      "product": {
        "id": "prd_Zt6Pa2Rn",
        "name": "Baked Beans In Tomato Sauce 410g",
        "url": "https://northwindwholesale.example.com/baked-beans-in-tomato-sauce-410g"
      },
      "price": {
        "amount": "17.99",
        "was": null,
        "currency": "ZAR"
      },
      "last_collected_at": "2026-11-02T03:20:05Z",
      "availability": "unknown",
      "status": "active",
      "stale": false,
      "match": {
        "review": "unreviewed",
        "reviewed_by": null,
        "confidence": 0.81
      }
    },
    {
      "competitor": {
        "id": "cmp_2HdR6wJ3",
        "name": "Metro Bulk",
        "domain": "metrobulk.example.com"
      },
      "product": {
        "id": "prd_Yc1Ke7Fs",
        "name": "Baked Beans in Tomato Sauce 24 x 410g",
        "url": "https://metrobulk.example.com/baked-beans-24x410g"
      },
      "price": {
        "amount": "429.00",
        "was": null,
        "currency": "ZAR"
      },
      "last_collected_at": "2026-10-24T04:31:58Z",
      "availability": "unknown",
      "status": "active",
      "stale": true,
      "match": {
        "review": "with_differences",
        "reviewed_by": "customer",
        "confidence": null
      }
    }
  ],
  "integrity": "ok",
  "updated_at": "2026-11-02T04:14:00Z"
}

Attributes

  • id#string

    Opaque comparison set id.

    Example
    set_x7Kp2Lm9
  • name#string
  • sku#stringnullable

    Your SKU for this set, or null if none is linked.

  • own_product#objectnullable

    Your product in this set, or null if the set has no product of yours.

    Show child attributesHide child attributes
    • id#string

      Opaque product id. Stable across URL changes and re-collection.

      Example
      prd_8fK2mQa1
    • name#string
    • url#string
    • price#objectnullable

      Your current price, or null if none has been collected.

      Show child attributesHide child attributes
      • amount#string

        The price a shopper pays now. During a promotion, this is the discounted price. "Cheapest" and "position" in GetPrice always use amount.

        Example
        18.99
      • was#stringnullable

        The original (pre-discount) price the retailer shows during a promotion, from the same price reading as amount. Sent only when strictly greater than amount; otherwise null (not on promotion).

        Example
        18.99
      • currency#string

        ISO 4217 code. Always present.

        Example
        ZAR
    • last_collected_at#timestampnullable

      The last time GetPrice successfully read this product's price, whether or not it changed. Failed reads, and readings rejected by our quality checks, don't move it; you keep the previous good price and its time. It is not "when the price last changed". null when price is null.

      Example
      2026-11-02T04:12:09Z
    • stale#boolean

      true when last_collected_at is older than your refresh schedule's longest normal gap plus 24 hours (for a Monday/Wednesday/Friday schedule: 96 hours), or there is no price. The price is still returned; treat it with care.

  • competitors#array of objects

    Every competitor in the set, as the dashboard shows it. [] when there are none.

    Show child attributesHide child attributes
    • competitor#object
      Show child attributesHide child attributes
      • id#string

        Opaque competitor (website) id.

        Example
        cmp_3LqV8nP2
      • name#string
        Example
        Harbour Market
      • domain#string
        Example
        harbourmarket.example.com
    • product#object
      Show child attributesHide child attributes
      • id#string

        Opaque product id. Stable across URL changes and re-collection.

        Example
        prd_8fK2mQa1
      • name#string
      • url#string
    • price#objectnullable

      The competitor's current price, or null if no valid price is available.

      Show child attributesHide child attributes
      • amount#string

        The price a shopper pays now. During a promotion, this is the discounted price. "Cheapest" and "position" in GetPrice always use amount.

        Example
        18.99
      • was#stringnullable

        The original (pre-discount) price the retailer shows during a promotion, from the same price reading as amount. Sent only when strictly greater than amount; otherwise null (not on promotion).

        Example
        18.99
      • currency#string

        ISO 4217 code. Always present.

        Example
        ZAR
    • last_collected_at#timestampnullable

      The last time GetPrice successfully read this product's price, whether or not it changed. Failed reads, and readings rejected by our quality checks, don't move it; you keep the previous good price and its time. It is not "when the price last changed". null when price is null.

      Example
      2026-11-02T04:12:09Z
    • availability#string

      Stock status on the competitor's site. GetPrice does not collect stock yet, so this is unknown for every listing for now. Other values will appear without notice; treat this as an open set.

      One of (more may be added)
      unknown, in_stock, out_of_stock
    • status#string

      active while the product is listed on the competitor's site. delisted products are only returned with include_delisted=true.

      One of (more may be added)
      active, delisted
    • stale#boolean

      true when last_collected_at is older than your refresh schedule's longest normal gap plus 24 hours (for a Monday/Wednesday/Friday schedule: 96 hours), or there is no price. The price is still returned; treat it with care.

    • match#object

      How this competitor product came to be in the set.

      Show child attributesHide child attributes
      • review#string

        same — confirmed as the same item. with_differences — kept on purpose despite a difference (for example pack size). unreviewed — added to the set but not yet confirmed; GetPrice leaves these out of "cheapest" and "position" in the dashboard. Open set.

        One of (more may be added)
        same, with_differences, unreviewed
      • reviewed_by#stringnullable

        Who confirmed it: customer — one of your own users; getprice — GetPrice's matching team or review process; null — not confirmed by anyone yet. Open set.

        One of (more may be added)
        customer, getprice
      • confidence#numbernullable

        The matcher's confidence from 0 to 1 when a machine match backs this entry, otherwise null.

        Range
        0 to 1
  • integrity#string

    ok — at most one listing per competitor. retailer_conflict — the set holds more than one listing from the same retailer (for example two pack sizes). All of them are returned; treat the set with care, e.g. take the lowest price per retailer or flag the set for review. Open set.

    One of (more may be added)
    ok, retailer_conflict
  • updated_at#timestamp

    When the set's contents last changed (not on every price read).

    Example
    2026-11-02T04:12:09Z

List comparison sets#

Requires matches:read

Returns your comparison sets: each of your products with every competitor's price and when it was last collected.

Call it in one of two ways:

  • Look up SKUs. Repeat sku for up to 100 of your SKUs: ?sku=10072L&sku=11247P. data holds a set for each SKU that has one, and skus gives each SKU's status: found, no_comparison_set or unknown_sku.
  • Sync everything. Leave out sku and follow next_cursor until it is null.

Rate-limit cost: with sku, 1 unit per 25 SKUs (rounded up); without it, 1 unit per 100 of limit (rounded up). Every other endpoint costs 1 unit.

Pack and case codes answer unknown_sku until you link them. GetPrice never converts unit prices into pack prices. Suppressed prices and rejected matches are never returned. Delisted competitor products appear only with include_delisted=true.

BASE=https://api.getprice.ai/v1
curl -G "$BASE/comparison-sets" \
  -H "Authorization: Bearer $GETPRICE_API_KEY" \
  --data-urlencode "sku=10072L" \
  --data-urlencode "sku=11247P"
import os

import requests

BASE = "https://api.getprice.ai/v1"
KEY = os.environ["GETPRICE_API_KEY"]

resp = requests.get(
    f"{BASE}/comparison-sets",
    params=[("sku", "10072L"), ("sku", "11247P")],
    headers={"Authorization": f"Bearer {KEY}"},
    timeout=30,
)
resp.raise_for_status()
page = resp.json()
for entry in page["skus"]:
    print(entry["sku"], entry["status"])
for cset in page["data"]:
    for entry in cset["competitors"]:
        price = entry["price"]
        if price:
            name = entry["competitor"]["name"]
            print(cset["sku"], name, price["amount"])
interface Entry {
  competitor: { name: string };
  price: { amount: string } | null;
}
interface Page {
  data: { sku: string | null; competitors: Entry[] }[];
  skus: { sku: string; status: string }[];
}

const BASE = "https://api.getprice.ai/v1";
const KEY = process.env.GETPRICE_API_KEY;
const params = new URLSearchParams([
  ["sku", "10072L"],
  ["sku", "11247P"],
]);
const url = `${BASE}/comparison-sets?${params}`;
const res = await fetch(url, {
  headers: { Authorization: `Bearer ${KEY}` },
});
if (!res.ok) {
  throw new Error(`GetPrice returned ${res.status}`);
}
const page = (await res.json()) as Page;
for (const { sku, status } of page.skus) {
  console.log(sku, status);
}
for (const set of page.data) {
  for (const { competitor, price } of set.competitors) {
    if (!price) continue;
    console.log(set.sku, competitor.name, price.amount);
  }
}
<?php
$base = "https://api.getprice.ai/v1";
$key = getenv("GETPRICE_API_KEY");
// Repeat sku once per SKU.
$skus = ["10072L", "11247P"];
$query = implode("&", array_map(
    fn ($sku) => "sku=" . rawurlencode($sku),
    $skus,
));
$ch = curl_init("$base/comparison-sets?$query");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer $key",
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($body === false || $status >= 400) {
    throw new RuntimeException(
        "GetPrice returned $status: $body"
    );
}
$data = json_decode(
    $body, true, flags: JSON_THROW_ON_ERROR
);

foreach ($data["skus"] as $entry) {
    echo $entry["sku"], " ", $entry["status"], "\n";
}
foreach ($data["data"] as $set) {
    foreach ($set["competitors"] as $entry) {
        if ($entry["price"] === null) {
            continue;
        }
        echo $set["sku"], " ",
            $entry["competitor"]["name"], " ",
            $entry["price"]["amount"], "\n";
    }
}
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using Naming = System.Text.Json.JsonNamingPolicy;

var key = Environment.GetEnvironmentVariable(
    "GETPRICE_API_KEY");
var api = new Uri("https://api.getprice.ai/v1/");
var http = new HttpClient { BaseAddress = api };
http.Timeout = TimeSpan.FromSeconds(30);
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", key);
var json = new JsonSerializerOptions
{
    PropertyNamingPolicy = Naming.SnakeCaseLower,
};

var res = await http.GetAsync(
    "comparison-sets?sku=10072L&sku=11247P");
res.EnsureSuccessStatusCode();
var page = await res.Content
    .ReadFromJsonAsync<Page>(json);
foreach (var s in page!.Skus)
    Console.WriteLine($"{s.Sku} {s.Status}");
foreach (var set in page.Data)
    foreach (var e in set.Competitors)
        if (e.Price is { } price)
            Console.WriteLine(
                $"{set.Sku} {e.Competitor.Name} "
                + price.Amount);

record Page(List<CompSet> Data, List<SkuStatus> Skus);
record SkuStatus(string Sku, string Status);
record CompSet(string? Sku, List<Entry> Competitors);
record Entry(Competitor Competitor, Price? Price);
record Competitor(string Name);
record Price(string Amount);
from decimal import Decimal

import requests
from odoo import models

API = "https://api.getprice.ai/v1"


class ProductProduct(models.Model):
    _inherit = "product.product"

    def action_getprice_competitor_prices(self):
        """Post the cheapest fresh competitor price.

        Up to 100 products per call.
        """
        icp = self.env["ir.config_parameter"].sudo()
        key = icp.get_param("getprice.api_key")
        resp = requests.get(
            f"{API}/comparison-sets",
            params=[
                ("sku", p.default_code) for p in self
            ],
            headers={"Authorization": f"Bearer {key}"},
            timeout=30,
        )
        resp.raise_for_status()
        sets = {
            s["sku"].strip().upper(): s
            for s in resp.json()["data"]
        }
        for product in self:
            sku = product.default_code.strip().upper()
            cset = sets.get(sku, {})
            entries = cset.get("competitors", [])
            offers = [
                e
                for e in entries
                if e["price"]
                and not e["stale"]
                and e["match"]["review"] == "same"
            ]
            if not offers:
                continue
            best = min(
                offers,
                key=lambda e: Decimal(
                    e["price"]["amount"]
                ),
            )
            name = best["competitor"]["name"]
            price = best["price"]
            line = f"{name}: R{price['amount']}"
            if price["was"]:  # on promotion
                line += f" (was R{price['was']})"
            product.message_post(
                body=f"Cheapest: {line}"
            )
Response
{
  "data": [
    {
      "id": "set_x7Kp2Lm9",
      "name": "Baked Beans in Tomato Sauce 410g",
      "sku": "10072L",
      "own_product": {
        "id": "prd_8fK2mQa1",
        "name": "Baked Beans in Tomato Sauce 410g",
        "url": "https://shop.example.com/product/10072L",
        "price": {
          "amount": "18.99",
          "was": null,
          "currency": "ZAR"
        },
        "last_collected_at": "2026-11-02T04:12:09Z",
        "stale": false
      },
      "competitors": [
        {
          "competitor": {
            "id": "cmp_3LqV8nP2",
            "name": "Harbour Market",
            "domain": "harbourmarket.example.com"
          },
          "product": {
            "id": "prd_Q2vN9cD4",
            "name": "Baked Beans 410g",
            "url": "https://harbourmarket.example.com/baked-beans-in-tomato-sauce-410g"
          },
          "price": {
            "amount": "17.99",
            "was": "19.99",
            "currency": "ZAR"
          },
          "last_collected_at": "2026-11-02T03:05:11Z",
          "availability": "unknown",
          "status": "active",
          "stale": false,
          "match": {
            "review": "same",
            "reviewed_by": "customer",
            "confidence": null
          }
        },
        {
          "competitor": {
            "id": "cmp_7RwK4tB9",
            "name": "Cape Fresh",
            "domain": "capefresh.example.com"
          },
          "product": {
            "id": "prd_Hm3Wc8Kp",
            "name": "Baked Beans In Tomato Sauce 410g",
            "url": "https://capefresh.example.com/baked-beans-in-tomato-sauce-410g"
          },
          "price": {
            "amount": "18.99",
            "was": null,
            "currency": "ZAR"
          },
          "last_collected_at": "2026-11-02T03:11:40Z",
          "availability": "unknown",
          "status": "active",
          "stale": false,
          "match": {
            "review": "same",
            "reviewed_by": "getprice",
            "confidence": 0.94
          }
        },
        {
          "competitor": {
            "id": "cmp_9TzM2cX5",
            "name": "Northwind Wholesale",
            "domain": "northwindwholesale.example.com"
          },
          "product": {
            "id": "prd_Zt6Pa2Rn",
            "name": "Baked Beans In Tomato Sauce 410g",
            "url": "https://northwindwholesale.example.com/baked-beans-in-tomato-sauce-410g"
          },
          "price": {
            "amount": "17.99",
            "was": null,
            "currency": "ZAR"
          },
          "last_collected_at": "2026-11-02T03:20:05Z",
          "availability": "unknown",
          "status": "active",
          "stale": false,
          "match": {
            "review": "unreviewed",
            "reviewed_by": null,
            "confidence": 0.81
          }
        },
        {
          "competitor": {
            "id": "cmp_2HdR6wJ3",
            "name": "Metro Bulk",
            "domain": "metrobulk.example.com"
          },
          "product": {
            "id": "prd_Yc1Ke7Fs",
            "name": "Baked Beans in Tomato Sauce 24 x 410g",
            "url": "https://metrobulk.example.com/baked-beans-24x410g"
          },
          "price": {
            "amount": "429.00",
            "was": null,
            "currency": "ZAR"
          },
          "last_collected_at": "2026-10-24T04:31:58Z",
          "availability": "unknown",
          "status": "active",
          "stale": true,
          "match": {
            "review": "with_differences",
            "reviewed_by": "customer",
            "confidence": null
          }
        }
      ],
      "integrity": "ok",
      "updated_at": "2026-11-02T04:14:00Z"
    }
  ],
  "next_cursor": null,
  "skus": [
    {
      "sku": "10072L",
      "status": "found",
      "set_id": "set_x7Kp2Lm9",
      "product_id": "prd_8fK2mQa1"
    },
    {
      "sku": "11893L",
      "status": "no_comparison_set",
      "set_id": null,
      "product_id": "prd_Zt41Hq6m"
    },
    {
      "sku": "11247P",
      "status": "unknown_sku",
      "set_id": null,
      "product_id": null
    }
  ]
}

Query parameters

  • sku#array of strings

    Your SKU. Repeat the parameter for several SKUs (?sku=A&sku=B), up to 100. Commas are part of the SKU, not separators. Matching ignores case and surrounding spaces; every other character (including leading zeros) counts: 00981A and 981A are different SKUs.

    Max items
    100
  • reviewed_by#string

    customer returns only competitors your own users have confirmed. Omit for every competitor in the set.

    One of
    customer
  • include_delisted#boolean

    true also returns competitor products that are no longer listed (status: delisted).

    Default
    false
  • limit#integer

    Items per page. Default 100. Values above 500 are treated as 500.

    Default
    100
  • cursor#string

    The next_cursor from the previous page. Opaque — pass it back unchanged, together with the same filters, on the same endpoint, as the first request (otherwise 400 cursor_mismatch).

Headers

  • If-None-Match#string

    An ETag from a previous response. If nothing changed you get 304 Not Modified with no body.

Returns

200

A page of comparison sets.

  • next_cursor#stringnullable

    Pass as cursor to get the next page. null on the last page.

  • skus#array of objects

    Only when you filter by sku, and only on the first page: one entry per SKU you sent, in the order you sent them, saying what happened to it.

    Show child attributesHide child attributes
    • sku#string

      The SKU as you sent it (trimmed).

      Example
      10072L
    • status#string

      found — linked to your product, which has a comparison set (in data). no_comparison_set — linked to your product, but no comparison set has been built for it yet. unknown_sku — not linked to any of your products. The stock codes on your online shop are pre-loaded, so this is usually a pack, case or internal code; link it with PUT /v1/sku-mappings/{sku}. Open set.

      One of (more may be added)
      found, no_comparison_set, unknown_sku
    • set_id#stringnullable

      Opaque comparison set id.

      Example
      set_x7Kp2Lm9
    • product_id#stringnullable

      Opaque product id. Stable across URL changes and re-collection.

      Example
      prd_8fK2mQa1

Errors

  • 400

    The request is malformed: an invalid or unknown query parameter, a bad cursor, more than 100 sku values, or an invalid SKU in the path (empty after trimming or over 100 characters).

  • 401

    No key (unauthenticated), or a key that is malformed, unknown, revoked or expired (invalid_key; for security the response doesn't say which).

  • 403

    The key is valid but lacks the scope this endpoint needs.

  • 429

    This request costs more rate-limit units than you have left.

  • 500

    Something went wrong on our side.

  • 503

    Temporarily unavailable: GetPrice can't guarantee correct match data right now, or the database is saturated.

How errors are shaped

Retrieve a comparison set#

Requires matches:read

Returns the comparison set with the given id. If the set was merged into another one, you get the surviving set, and its id differs from the one you asked for.

BASE=https://api.getprice.ai/v1
curl "$BASE/comparison-sets/set_x7Kp2Lm9" \
  -H "Authorization: Bearer $GETPRICE_API_KEY"
import os

import requests

BASE = "https://api.getprice.ai/v1"
KEY = os.environ["GETPRICE_API_KEY"]

resp = requests.get(
    f"{BASE}/comparison-sets/set_x7Kp2Lm9",
    headers={"Authorization": f"Bearer {KEY}"},
    timeout=30,
)
resp.raise_for_status()
cset = resp.json()
for entry in cset["competitors"]:
    price = entry["price"]
    amount = f"R{price['amount']}" if price else "-"
    name = entry["competitor"]["name"]
    print(name, amount, entry["last_collected_at"])
interface Entry {
  competitor: { name: string };
  price: { amount: string } | null;
  last_collected_at: string | null;
}

const BASE = "https://api.getprice.ai/v1";
const KEY = process.env.GETPRICE_API_KEY;
const url = `${BASE}/comparison-sets/set_x7Kp2Lm9`;
const res = await fetch(url, {
  headers: { Authorization: `Bearer ${KEY}` },
});
if (!res.ok) {
  throw new Error(`GetPrice returned ${res.status}`);
}
const set = (await res.json()) as {
  competitors: Entry[];
};
for (const entry of set.competitors) {
  const { price } = entry;
  const amount = price ? `R${price.amount}` : "-";
  const name = entry.competitor.name;
  console.log(name, amount, entry.last_collected_at);
}
<?php
$base = "https://api.getprice.ai/v1";
$key = getenv("GETPRICE_API_KEY");
$ch = curl_init("$base/comparison-sets/set_x7Kp2Lm9");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer $key",
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($body === false || $status >= 400) {
    throw new RuntimeException(
        "GetPrice returned $status: $body"
    );
}
$data = json_decode(
    $body, true, flags: JSON_THROW_ON_ERROR
);

foreach ($data["competitors"] as $entry) {
    $price = $entry["price"];
    $amount = $price ? "R" . $price["amount"] : "-";
    echo $entry["competitor"]["name"], " ", $amount,
        " ", $entry["last_collected_at"], "\n";
}
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using Naming = System.Text.Json.JsonNamingPolicy;

var key = Environment.GetEnvironmentVariable(
    "GETPRICE_API_KEY");
var api = new Uri("https://api.getprice.ai/v1/");
var http = new HttpClient { BaseAddress = api };
http.Timeout = TimeSpan.FromSeconds(30);
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", key);
var json = new JsonSerializerOptions
{
    PropertyNamingPolicy = Naming.SnakeCaseLower,
};

var res = await http.GetAsync(
    "comparison-sets/set_x7Kp2Lm9");
res.EnsureSuccessStatusCode();
var set = await res.Content
    .ReadFromJsonAsync<CompSet>(json);
foreach (var e in set!.Competitors)
{
    var amount = e.Price is null
        ? "-" : $"R{e.Price.Amount}";
    var name = e.Competitor.Name;
    Console.WriteLine(
        $"{name} {amount} {e.LastCollectedAt}");
}

record CompSet(List<Entry> Competitors);
record Entry(
    Competitor Competitor,
    Price? Price,
    string? LastCollectedAt);
record Competitor(string Name);
record Price(string Amount);
import requests
from odoo import models

API = "https://api.getprice.ai/v1"


class ProductProduct(models.Model):
    _inherit = "product.product"

    def getprice_post_comparison_set(self, set_id):
        """Post every competitor price in one set."""
        self.ensure_one()
        icp = self.env["ir.config_parameter"].sudo()
        key = icp.get_param("getprice.api_key")
        resp = requests.get(
            f"{API}/comparison-sets/{set_id}",
            headers={"Authorization": f"Bearer {key}"},
            timeout=30,
        )
        resp.raise_for_status()
        lines = []
        for e in resp.json()["competitors"]:
            name = e["competitor"]["name"]
            price = e["price"]
            amount = "-"
            if price:
                amount = f"R{price['amount']}"
            if price and price["was"]:  # on promotion
                amount += f" (was R{price['was']})"
            lines.append(f"{name}: {amount}")
        self.message_post(body="; ".join(lines))
Response
{
  "id": "set_x7Kp2Lm9",
  "name": "Baked Beans in Tomato Sauce 410g",
  "sku": "10072L",
  "own_product": {
    "id": "prd_8fK2mQa1",
    "name": "Baked Beans in Tomato Sauce 410g",
    "url": "https://shop.example.com/product/10072L",
    "price": {
      "amount": "18.99",
      "was": null,
      "currency": "ZAR"
    },
    "last_collected_at": "2026-11-02T04:12:09Z",
    "stale": false
  },
  "competitors": [
    {
      "competitor": {
        "id": "cmp_3LqV8nP2",
        "name": "Harbour Market",
        "domain": "harbourmarket.example.com"
      },
      "product": {
        "id": "prd_Q2vN9cD4",
        "name": "Baked Beans 410g",
        "url": "https://harbourmarket.example.com/baked-beans-in-tomato-sauce-410g"
      },
      "price": {
        "amount": "17.99",
        "was": "19.99",
        "currency": "ZAR"
      },
      "last_collected_at": "2026-11-02T03:05:11Z",
      "availability": "unknown",
      "status": "active",
      "stale": false,
      "match": {
        "review": "same",
        "reviewed_by": "customer",
        "confidence": null
      }
    },
    {
      "competitor": {
        "id": "cmp_7RwK4tB9",
        "name": "Cape Fresh",
        "domain": "capefresh.example.com"
      },
      "product": {
        "id": "prd_Hm3Wc8Kp",
        "name": "Baked Beans In Tomato Sauce 410g",
        "url": "https://capefresh.example.com/baked-beans-in-tomato-sauce-410g"
      },
      "price": {
        "amount": "18.99",
        "was": null,
        "currency": "ZAR"
      },
      "last_collected_at": "2026-11-02T03:11:40Z",
      "availability": "unknown",
      "status": "active",
      "stale": false,
      "match": {
        "review": "same",
        "reviewed_by": "getprice",
        "confidence": 0.94
      }
    },
    {
      "competitor": {
        "id": "cmp_9TzM2cX5",
        "name": "Northwind Wholesale",
        "domain": "northwindwholesale.example.com"
      },
      "product": {
        "id": "prd_Zt6Pa2Rn",
        "name": "Baked Beans In Tomato Sauce 410g",
        "url": "https://northwindwholesale.example.com/baked-beans-in-tomato-sauce-410g"
      },
      "price": {
        "amount": "17.99",
        "was": null,
        "currency": "ZAR"
      },
      "last_collected_at": "2026-11-02T03:20:05Z",
      "availability": "unknown",
      "status": "active",
      "stale": false,
      "match": {
        "review": "unreviewed",
        "reviewed_by": null,
        "confidence": 0.81
      }
    },
    {
      "competitor": {
        "id": "cmp_2HdR6wJ3",
        "name": "Metro Bulk",
        "domain": "metrobulk.example.com"
      },
      "product": {
        "id": "prd_Yc1Ke7Fs",
        "name": "Baked Beans in Tomato Sauce 24 x 410g",
        "url": "https://metrobulk.example.com/baked-beans-24x410g"
      },
      "price": {
        "amount": "429.00",
        "was": null,
        "currency": "ZAR"
      },
      "last_collected_at": "2026-10-24T04:31:58Z",
      "availability": "unknown",
      "status": "active",
      "stale": true,
      "match": {
        "review": "with_differences",
        "reviewed_by": "customer",
        "confidence": null
      }
    }
  ],
  "integrity": "ok",
  "updated_at": "2026-11-02T04:14:00Z"
}

Path parameters

  • set_id#stringRequired

    The set id, e.g. set_x7Kp2Lm9.

    Example
    set_x7Kp2Lm9

Query parameters

  • reviewed_by#string

    customer returns only competitors your own users have confirmed. Omit for every competitor in the set.

    One of
    customer
  • include_delisted#boolean

    true also returns competitor products that are no longer listed (status: delisted).

    Default
    false

Headers

  • If-None-Match#string

    An ETag from a previous response. If nothing changed you get 304 Not Modified with no body.

Returns

200

The comparison set.

Returns the comparison set object.

Errors

  • 400

    The request is malformed: an invalid or unknown query parameter, a bad cursor, more than 100 sku values, or an invalid SKU in the path (empty after trimming or over 100 characters).

  • 401

    No key (unauthenticated), or a key that is malformed, unknown, revoked or expired (invalid_key; for security the response doesn't say which).

  • 403

    The key is valid but lacks the scope this endpoint needs.

  • 404

    Not found for your company.

  • 429

    This request costs more rate-limit units than you have left.

  • 500

    Something went wrong on our side.

  • 503

    Temporarily unavailable: GetPrice can't guarantee correct match data right now, or the database is saturated.

How errors are shaped

The product object#

The product object
{
  "id": "prd_Lp4sT1x9",
  "name": "Pilchards in Tomato Sauce 400g",
  "url": "https://shop.example.com/product/pilchards-in-tomato-sauce-400g",
  "skus": [],
  "comparison_set_id": "set_R4m8wQe2",
  "price": {
    "amount": "31.99",
    "was": "33.99",
    "currency": "ZAR"
  },
  "last_collected_at": "2026-11-02T04:12:31Z",
  "status": "active"
}

Attributes

  • id#string

    Opaque product id. Stable across URL changes and re-collection.

    Example
    prd_8fK2mQa1
  • name#string
  • url#string
  • skus#array of strings

    Every SKU you have linked to this product. [] when none.

  • comparison_set_id#stringnullable

    The comparison set this product sits in, or null if it is not in one yet.

    Example
    set_x7Kp2Lm9
  • price#objectnullable

    A price as read from a website. When it was read is the sibling last_collected_at.

    Show child attributesHide child attributes
    • amount#string

      The price a shopper pays now. During a promotion, this is the discounted price. "Cheapest" and "position" in GetPrice always use amount.

      Example
      18.99
    • was#stringnullable

      The original (pre-discount) price the retailer shows during a promotion, from the same price reading as amount. Sent only when strictly greater than amount; otherwise null (not on promotion).

      Example
      18.99
    • currency#string

      ISO 4217 code. Always present.

      Example
      ZAR
  • last_collected_at#timestampnullable

    RFC 3339, UTC, Z suffix.

    Example
    2026-11-02T04:12:09Z
  • status#string

    active, or delisted when the product is no longer on your site (only with include_delisted=true). Open set.

    One of (more may be added)
    active, delisted

List your products#

Requires catalog:read

Returns your own products, as GetPrice collects them from your online shop. Search by name with q, and add unmapped=true to find products with no SKU linked. That gives you the prd_… id to link with PUT /v1/sku-mappings/{sku}. Filters combine.

BASE=https://api.getprice.ai/v1
curl -G "$BASE/products" \
  -H "Authorization: Bearer $GETPRICE_API_KEY" \
  --data-urlencode "q=pilchards" \
  --data-urlencode "unmapped=true"
import os

import requests

BASE = "https://api.getprice.ai/v1"
KEY = os.environ["GETPRICE_API_KEY"]

resp = requests.get(
    f"{BASE}/products",
    params=[("q", "pilchards"), ("unmapped", "true")],
    headers={"Authorization": f"Bearer {KEY}"},
    timeout=30,
)
resp.raise_for_status()
for product in resp.json()["data"]:
    print(product["id"], product["name"])
interface Product {
  id: string;
  name: string;
}

const BASE = "https://api.getprice.ai/v1";
const KEY = process.env.GETPRICE_API_KEY;
const params = new URLSearchParams([
  ["q", "pilchards"],
  ["unmapped", "true"],
]);
const url = `${BASE}/products?${params}`;
const res = await fetch(url, {
  headers: { Authorization: `Bearer ${KEY}` },
});
if (!res.ok) {
  throw new Error(`GetPrice returned ${res.status}`);
}
const page = (await res.json()) as {
  data: Product[];
};
for (const product of page.data) {
  console.log(product.id, product.name);
}
<?php
$base = "https://api.getprice.ai/v1";
$key = getenv("GETPRICE_API_KEY");
$query = http_build_query([
    "q" => "pilchards",
    "unmapped" => "true",
]);
$ch = curl_init("$base/products?$query");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer $key",
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($body === false || $status >= 400) {
    throw new RuntimeException(
        "GetPrice returned $status: $body"
    );
}
$data = json_decode(
    $body, true, flags: JSON_THROW_ON_ERROR
);

foreach ($data["data"] as $product) {
    echo $product["id"], " ", $product["name"], "\n";
}
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using Naming = System.Text.Json.JsonNamingPolicy;

var key = Environment.GetEnvironmentVariable(
    "GETPRICE_API_KEY");
var api = new Uri("https://api.getprice.ai/v1/");
var http = new HttpClient { BaseAddress = api };
http.Timeout = TimeSpan.FromSeconds(30);
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", key);
var json = new JsonSerializerOptions
{
    PropertyNamingPolicy = Naming.SnakeCaseLower,
};

var res = await http.GetAsync(
    "products?q=pilchards&unmapped=true");
res.EnsureSuccessStatusCode();
var page = await res.Content
    .ReadFromJsonAsync<Page>(json);
foreach (var p in page!.Data)
    Console.WriteLine($"{p.Id} {p.Name}");

record Page(List<Product> Data);
record Product(string Id, string Name);
import requests
from odoo import models

API = "https://api.getprice.ai/v1"


class ProductProduct(models.Model):
    _inherit = "product.product"

    def action_getprice_find_matches(self):
        """Suggest unlinked products by name."""
        self.ensure_one()
        icp = self.env["ir.config_parameter"].sudo()
        key = icp.get_param("getprice.api_key")
        resp = requests.get(
            f"{API}/products",
            params={"q": self.name, "unmapped": "true"},
            headers={"Authorization": f"Bearer {key}"},
            timeout=30,
        )
        resp.raise_for_status()
        found = [
            f"{p['id']} {p['name']}"
            for p in resp.json()["data"]
        ]
        text = "; ".join(found) or "none"
        self.message_post(body=f"Candidates: {text}")
Response
200
{
  "data": [
    {
      "id": "prd_Lp4sT1x9",
      "name": "Pilchards in Tomato Sauce 400g",
      "url": "https://shop.example.com/product/pilchards-in-tomato-sauce-400g",
      "skus": [],
      "comparison_set_id": "set_R4m8wQe2",
      "price": {
        "amount": "31.99",
        "was": "33.99",
        "currency": "ZAR"
      },
      "last_collected_at": "2026-11-02T04:12:31Z",
      "status": "active"
    }
  ],
  "next_cursor": null
}

Query parameters

  • sku#array of strings

    Your SKU. Repeat the parameter for several SKUs (?sku=A&sku=B), up to 100. Commas are part of the SKU, not separators. Matching ignores case and surrounding spaces; every other character (including leading zeros) counts: 00981A and 981A are different SKUs.

    Max items
    100
  • q#string

    Case-insensitive substring of the product name.

  • unmapped#boolean

    true returns only products with no SKU linked.

    Default
    false
  • include_delisted#boolean

    true also returns products no longer on your site (status: delisted).

    Default
    false
  • limit#integer

    Items per page. Default 100. Values above 500 are treated as 500.

    Default
    100
  • cursor#string

    The next_cursor from the previous page. Opaque — pass it back unchanged, together with the same filters, on the same endpoint, as the first request (otherwise 400 cursor_mismatch).

Headers

  • If-None-Match#string

    An ETag from a previous response. If nothing changed you get 304 Not Modified with no body.

Returns

200

A page of products.

Errors

  • 400

    The request is malformed: an invalid or unknown query parameter, a bad cursor, more than 100 sku values, or an invalid SKU in the path (empty after trimming or over 100 characters).

  • 401

    No key (unauthenticated), or a key that is malformed, unknown, revoked or expired (invalid_key; for security the response doesn't say which).

  • 403

    The key is valid but lacks the scope this endpoint needs.

  • 429

    This request costs more rate-limit units than you have left.

  • 500

    Something went wrong on our side.

  • 503

    Temporarily unavailable: GetPrice can't guarantee correct match data right now, or the database is saturated.

How errors are shaped

Retrieve a product#

Requires catalog:read

Returns one of your own products. A competitor's product id, or an id from another company, returns 404.

BASE=https://api.getprice.ai/v1
curl "$BASE/products/prd_8fK2mQa1" \
  -H "Authorization: Bearer $GETPRICE_API_KEY"
import os

import requests

BASE = "https://api.getprice.ai/v1"
KEY = os.environ["GETPRICE_API_KEY"]

resp = requests.get(
    f"{BASE}/products/prd_8fK2mQa1",
    headers={"Authorization": f"Bearer {KEY}"},
    timeout=30,
)
resp.raise_for_status()
product = resp.json()
price = product["price"]
amount = f"R{price['amount']}" if price else "-"
print(product["name"], amount)
interface Product {
  name: string;
  price: { amount: string } | null;
}

const BASE = "https://api.getprice.ai/v1";
const KEY = process.env.GETPRICE_API_KEY;
const url = `${BASE}/products/prd_8fK2mQa1`;
const res = await fetch(url, {
  headers: { Authorization: `Bearer ${KEY}` },
});
if (!res.ok) {
  throw new Error(`GetPrice returned ${res.status}`);
}
const product = (await res.json()) as Product;
const { price } = product;
const amount = price ? `R${price.amount}` : "-";
console.log(product.name, amount);
<?php
$base = "https://api.getprice.ai/v1";
$key = getenv("GETPRICE_API_KEY");
$ch = curl_init("$base/products/prd_8fK2mQa1");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer $key",
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($body === false || $status >= 400) {
    throw new RuntimeException(
        "GetPrice returned $status: $body"
    );
}
$data = json_decode(
    $body, true, flags: JSON_THROW_ON_ERROR
);

$price = $data["price"];
$amount = $price ? "R" . $price["amount"] : "-";
echo $data["name"], " ", $amount, "\n";
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using Naming = System.Text.Json.JsonNamingPolicy;

var key = Environment.GetEnvironmentVariable(
    "GETPRICE_API_KEY");
var api = new Uri("https://api.getprice.ai/v1/");
var http = new HttpClient { BaseAddress = api };
http.Timeout = TimeSpan.FromSeconds(30);
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", key);
var json = new JsonSerializerOptions
{
    PropertyNamingPolicy = Naming.SnakeCaseLower,
};

var res = await http.GetAsync("products/prd_8fK2mQa1");
res.EnsureSuccessStatusCode();
var product = await res.Content
    .ReadFromJsonAsync<Product>(json);
var amount = product!.Price is null
    ? "-" : $"R{product.Price.Amount}";
Console.WriteLine($"{product.Name} {amount}");

record Product(string Name, Price? Price);
record Price(string Amount);
import requests
from odoo import models

API = "https://api.getprice.ai/v1"


class ProductProduct(models.Model):
    _inherit = "product.product"

    def getprice_post_shop_price(self, gp_product_id):
        """Post the price collected from your shop."""
        self.ensure_one()
        icp = self.env["ir.config_parameter"].sudo()
        key = icp.get_param("getprice.api_key")
        resp = requests.get(
            f"{API}/products/{gp_product_id}",
            headers={"Authorization": f"Bearer {key}"},
            timeout=30,
        )
        resp.raise_for_status()
        price = resp.json()["price"]
        if price:
            self.message_post(
                body=f"Shop price R{price['amount']}"
            )
Response
{
  "id": "prd_8fK2mQa1",
  "name": "Baked Beans in Tomato Sauce 410g",
  "url": "https://shop.example.com/product/10072L",
  "skus": [
    "10072L"
  ],
  "comparison_set_id": "set_x7Kp2Lm9",
  "price": {
    "amount": "18.99",
    "was": null,
    "currency": "ZAR"
  },
  "last_collected_at": "2026-11-02T04:12:09Z",
  "status": "active"
}

Path parameters

  • product_id#stringRequired

    The product id, e.g. prd_8fK2mQa1.

    Example
    prd_8fK2mQa1

Headers

  • If-None-Match#string

    An ETag from a previous response. If nothing changed you get 304 Not Modified with no body.

Returns

200

The product.

Returns the product object.

Errors

  • 400

    The request is malformed: an invalid or unknown query parameter, a bad cursor, more than 100 sku values, or an invalid SKU in the path (empty after trimming or over 100 characters).

  • 401

    No key (unauthenticated), or a key that is malformed, unknown, revoked or expired (invalid_key; for security the response doesn't say which).

  • 403

    The key is valid but lacks the scope this endpoint needs.

  • 404

    Not found for your company.

  • 429

    This request costs more rate-limit units than you have left.

  • 500

    Something went wrong on our side.

  • 503

    Temporarily unavailable: GetPrice can't guarantee correct match data right now, or the database is saturated.

How errors are shaped

The competitor object#

The competitor object
{
  "id": "cmp_3LqV8nP2",
  "name": "Harbour Market",
  "domain": "harbourmarket.example.com",
  "url": "https://harbourmarket.example.com"
}

Attributes

  • id#string

    Opaque competitor (website) id.

    Example
    cmp_3LqV8nP2
  • name#string
    Example
    Harbour Market
  • domain#string
    Example
    harbourmarket.example.com
  • url#string
    Example
    https://harbourmarket.example.com

List competitors#

Requires catalog:read

Returns the competitor websites GetPrice collects prices from for you.

BASE=https://api.getprice.ai/v1
curl "$BASE/competitors" \
  -H "Authorization: Bearer $GETPRICE_API_KEY"
import os

import requests

BASE = "https://api.getprice.ai/v1"
KEY = os.environ["GETPRICE_API_KEY"]

resp = requests.get(
    f"{BASE}/competitors",
    headers={"Authorization": f"Bearer {KEY}"},
    timeout=30,
)
resp.raise_for_status()
for c in resp.json()["data"]:
    print(c["id"], c["name"], c["domain"])
interface Competitor {
  id: string;
  name: string;
  domain: string;
}

const BASE = "https://api.getprice.ai/v1";
const KEY = process.env.GETPRICE_API_KEY;
const url = `${BASE}/competitors`;
const res = await fetch(url, {
  headers: { Authorization: `Bearer ${KEY}` },
});
if (!res.ok) {
  throw new Error(`GetPrice returned ${res.status}`);
}
const page = (await res.json()) as {
  data: Competitor[];
};
for (const { id, name, domain } of page.data) {
  console.log(id, name, domain);
}
<?php
$base = "https://api.getprice.ai/v1";
$key = getenv("GETPRICE_API_KEY");
$ch = curl_init("$base/competitors");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer $key",
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($body === false || $status >= 400) {
    throw new RuntimeException(
        "GetPrice returned $status: $body"
    );
}
$data = json_decode(
    $body, true, flags: JSON_THROW_ON_ERROR
);

foreach ($data["data"] as $c) {
    echo $c["id"], " ", $c["name"], " ",
        $c["domain"], "\n";
}
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using Naming = System.Text.Json.JsonNamingPolicy;

var key = Environment.GetEnvironmentVariable(
    "GETPRICE_API_KEY");
var api = new Uri("https://api.getprice.ai/v1/");
var http = new HttpClient { BaseAddress = api };
http.Timeout = TimeSpan.FromSeconds(30);
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", key);
var json = new JsonSerializerOptions
{
    PropertyNamingPolicy = Naming.SnakeCaseLower,
};

var res = await http.GetAsync("competitors");
res.EnsureSuccessStatusCode();
var page = await res.Content
    .ReadFromJsonAsync<Page>(json);
foreach (var c in page!.Data)
    Console.WriteLine($"{c.Id} {c.Name} {c.Domain}");

record Page(List<Competitor> Data);
record Competitor(
    string Id,
    string Name,
    string Domain);
import requests
from odoo import api, models

API = "https://api.getprice.ai/v1"


class ResPartner(models.Model):
    _inherit = "res.partner"

    @api.model
    def getprice_sync_competitors(self):
        """Create a contact per tracked competitor."""
        icp = self.env["ir.config_parameter"].sudo()
        key = icp.get_param("getprice.api_key")
        resp = requests.get(
            f"{API}/competitors",
            headers={"Authorization": f"Bearer {key}"},
            timeout=30,
        )
        resp.raise_for_status()
        for c in resp.json()["data"]:
            domain = [("website", "=", c["url"])]
            if not self.search(domain, limit=1):
                self.create({
                    "name": c["name"],
                    "website": c["url"],
                    "is_company": True,
                })
Response
200
{
  "data": [
    {
      "id": "cmp_3LqV8nP2",
      "name": "Harbour Market",
      "domain": "harbourmarket.example.com",
      "url": "https://harbourmarket.example.com"
    },
    {
      "id": "cmp_7RwK4tB9",
      "name": "Cape Fresh",
      "domain": "capefresh.example.com",
      "url": "https://capefresh.example.com"
    },
    {
      "id": "cmp_9TzM2cX5",
      "name": "Northwind Wholesale",
      "domain": "northwindwholesale.example.com",
      "url": "https://northwindwholesale.example.com"
    },
    {
      "id": "cmp_2HdR6wJ3",
      "name": "Metro Bulk",
      "domain": "metrobulk.example.com",
      "url": "https://metrobulk.example.com"
    }
  ],
  "next_cursor": null
}

Query parameters

  • limit#integer

    Items per page. Default 100. Values above 500 are treated as 500.

    Default
    100
  • cursor#string

    The next_cursor from the previous page. Opaque — pass it back unchanged, together with the same filters, on the same endpoint, as the first request (otherwise 400 cursor_mismatch).

Headers

  • If-None-Match#string

    An ETag from a previous response. If nothing changed you get 304 Not Modified with no body.

Returns

200

A page of competitors.

Errors

  • 400

    The request is malformed: an invalid or unknown query parameter, a bad cursor, more than 100 sku values, or an invalid SKU in the path (empty after trimming or over 100 characters).

  • 401

    No key (unauthenticated), or a key that is malformed, unknown, revoked or expired (invalid_key; for security the response doesn't say which).

  • 403

    The key is valid but lacks the scope this endpoint needs.

  • 429

    This request costs more rate-limit units than you have left.

  • 500

    Something went wrong on our side.

  • 503

    Temporarily unavailable: GetPrice can't guarantee correct match data right now, or the database is saturated.

How errors are shaped

The SKU mapping object#

A link from your SKU to exactly one target: one of your products (product_id) or, for a comparison set with no product of yours, the set itself (comparison_set_id). Exactly one of the two is non-null. Links you make with PUT always target a product.

The SKU mapping object
{
  "sku": "10072L",
  "product_id": "prd_8fK2mQa1",
  "comparison_set_id": null,
  "source": "list_sku",
  "created_at": "2026-11-02T10:04:12Z",
  "updated_at": "2026-11-02T10:04:12Z"
}

Attributes

  • sku#string
    Example
    10072L
  • product_id#stringnullable

    The linked product, or null when the SKU is linked to a comparison set instead.

    Example
    prd_8fK2mQa1
  • comparison_set_id#stringnullable

    The linked comparison set when it has no product of yours (set up by GetPrice), otherwise null. A PUT to such a SKU replaces the link with a product link.

    Example
    set_x7Kp2Lm9
  • source#string

    How the link was made: api (through this API), list_sku (the SKU on a comparison set in the dashboard), site_sku (the code on your website), import / settings_upload (a file GetPrice loaded for you). Open set.

    One of (more may be added)
    api, list_sku, site_sku, import, settings_upload
  • created_at#timestamp

    RFC 3339, UTC, Z suffix.

    Example
    2026-11-02T04:12:09Z
  • updated_at#timestamp

    RFC 3339, UTC, Z suffix.

    Example
    2026-11-02T04:12:09Z

List SKU mappings#

Requires catalog:read

Returns every SKU link you have, ordered by SKU.

BASE=https://api.getprice.ai/v1
curl -G "$BASE/sku-mappings" \
  -H "Authorization: Bearer $GETPRICE_API_KEY" \
  --data-urlencode "limit=100"
import os

import requests

BASE = "https://api.getprice.ai/v1"
KEY = os.environ["GETPRICE_API_KEY"]

resp = requests.get(
    f"{BASE}/sku-mappings",
    params=[("limit", "100")],
    headers={"Authorization": f"Bearer {KEY}"},
    timeout=30,
)
resp.raise_for_status()
page = resp.json()
for m in page["data"]:
    target = m["product_id"] or m["comparison_set_id"]
    print(m["sku"], target)
# More? Send page["next_cursor"] as cursor.
interface SkuMapping {
  sku: string;
  product_id: string | null;
  comparison_set_id: string | null;
}

const BASE = "https://api.getprice.ai/v1";
const KEY = process.env.GETPRICE_API_KEY;
const params = new URLSearchParams([
  ["limit", "100"],
]);
const url = `${BASE}/sku-mappings?${params}`;
const res = await fetch(url, {
  headers: { Authorization: `Bearer ${KEY}` },
});
if (!res.ok) {
  throw new Error(`GetPrice returned ${res.status}`);
}
const page = (await res.json()) as {
  data: SkuMapping[];
  next_cursor: string | null;
};
for (const m of page.data) {
  const target = m.product_id ?? m.comparison_set_id;
  console.log(m.sku, target);
}
// More? Send page.next_cursor as cursor.
<?php
$base = "https://api.getprice.ai/v1";
$key = getenv("GETPRICE_API_KEY");
$query = http_build_query([
    "limit" => "100",
]);
$ch = curl_init("$base/sku-mappings?$query");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer $key",
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($body === false || $status >= 400) {
    throw new RuntimeException(
        "GetPrice returned $status: $body"
    );
}
$data = json_decode(
    $body, true, flags: JSON_THROW_ON_ERROR
);

foreach ($data["data"] as $m) {
    $target = $m["product_id"]
        ?? $m["comparison_set_id"];
    echo $m["sku"], " ", $target, "\n";
}
// More? Send $data["next_cursor"] as cursor.
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using Naming = System.Text.Json.JsonNamingPolicy;

var key = Environment.GetEnvironmentVariable(
    "GETPRICE_API_KEY");
var api = new Uri("https://api.getprice.ai/v1/");
var http = new HttpClient { BaseAddress = api };
http.Timeout = TimeSpan.FromSeconds(30);
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", key);
var json = new JsonSerializerOptions
{
    PropertyNamingPolicy = Naming.SnakeCaseLower,
};

var res = await http.GetAsync("sku-mappings?limit=100");
res.EnsureSuccessStatusCode();
var page = await res.Content
    .ReadFromJsonAsync<Page>(json);
foreach (var m in page!.Data)
    Console.WriteLine(
        $"{m.Sku} {m.ProductId ?? m.ComparisonSetId}");
// More? Send page.NextCursor as cursor.

record Page(List<SkuMapping> Data, string? NextCursor);
record SkuMapping(
    string Sku,
    string? ProductId,
    string? ComparisonSetId);
import logging

import requests
from odoo import api, models

API = "https://api.getprice.ai/v1"
_logger = logging.getLogger(__name__)


class ProductProduct(models.Model):
    _inherit = "product.product"

    @api.model
    def getprice_unlinked_products(self):
        """Products whose reference has no SKU link."""
        icp = self.env["ir.config_parameter"].sudo()
        key = icp.get_param("getprice.api_key")
        linked, params = set(), {"limit": 100}
        while True:
            resp = requests.get(
                f"{API}/sku-mappings",
                params=params,
                headers={
                    "Authorization": f"Bearer {key}"
                },
                timeout=30,
            )
            resp.raise_for_status()
            page = resp.json()
            for m in page["data"]:
                linked.add(m["sku"].strip().upper())
            if page["next_cursor"] is None:
                break
            params["cursor"] = page["next_cursor"]
        products = self.search(
            [("default_code", "!=", False)]
        )
        unlinked = products.filtered(
            lambda p: p.default_code.strip().upper()
            not in linked
        )
        _logger.info("%s unlinked", len(unlinked))
        return unlinked
Response
200
{
  "data": [
    {
      "sku": "10072L",
      "product_id": "prd_8fK2mQa1",
      "comparison_set_id": null,
      "source": "list_sku",
      "created_at": "2026-11-02T10:04:12Z",
      "updated_at": "2026-11-02T10:04:12Z"
    },
    {
      "sku": "13817L",
      "product_id": "prd_Lp4sT1x9",
      "comparison_set_id": null,
      "source": "api",
      "created_at": "2026-11-02T10:05:00Z",
      "updated_at": "2026-11-02T10:05:00Z"
    },
    {
      "sku": "15520L",
      "product_id": null,
      "comparison_set_id": "set_Wq3NdT5v",
      "source": "list_sku",
      "created_at": "2026-11-01T16:20:00Z",
      "updated_at": "2026-11-01T16:20:00Z"
    }
  ],
  "next_cursor": null
}

Query parameters

  • limit#integer

    Items per page. Default 100. Values above 500 are treated as 500.

    Default
    100
  • cursor#string

    The next_cursor from the previous page. Opaque — pass it back unchanged, together with the same filters, on the same endpoint, as the first request (otherwise 400 cursor_mismatch).

Headers

  • If-None-Match#string

    An ETag from a previous response. If nothing changed you get 304 Not Modified with no body.

Returns

200

A page of SKU mappings.

Errors

  • 400

    The request is malformed: an invalid or unknown query parameter, a bad cursor, more than 100 sku values, or an invalid SKU in the path (empty after trimming or over 100 characters).

  • 401

    No key (unauthenticated), or a key that is malformed, unknown, revoked or expired (invalid_key; for security the response doesn't say which).

  • 403

    The key is valid but lacks the scope this endpoint needs.

  • 429

    This request costs more rate-limit units than you have left.

  • 500

    Something went wrong on our side.

  • 503

    Temporarily unavailable: GetPrice can't guarantee correct match data right now, or the database is saturated.

How errors are shaped

Retrieve a SKU mapping#

Requires catalog:read

Returns what one of your SKUs is linked to. The response's ETag is the link's version, such as "v42". Send it as If-Match to replace or remove the link only if nobody has changed it since.

BASE=https://api.getprice.ai/v1
curl "$BASE/sku-mappings/10072L" \
  -H "Authorization: Bearer $GETPRICE_API_KEY"
import os

import requests

BASE = "https://api.getprice.ai/v1"
KEY = os.environ["GETPRICE_API_KEY"]

resp = requests.get(
    f"{BASE}/sku-mappings/10072L",
    headers={"Authorization": f"Bearer {KEY}"},
    timeout=30,
)
resp.raise_for_status()
mapping = resp.json()
print(mapping["sku"], mapping["product_id"])
print("ETag:", resp.headers["ETag"])
interface SkuMapping {
  sku: string;
  product_id: string | null;
}

const BASE = "https://api.getprice.ai/v1";
const KEY = process.env.GETPRICE_API_KEY;
const url = `${BASE}/sku-mappings/10072L`;
const res = await fetch(url, {
  headers: { Authorization: `Bearer ${KEY}` },
});
if (!res.ok) {
  throw new Error(`GetPrice returned ${res.status}`);
}
const mapping = (await res.json()) as SkuMapping;
console.log(mapping.sku, mapping.product_id);
console.log("ETag:", res.headers.get("etag"));
<?php
$base = "https://api.getprice.ai/v1";
$key = getenv("GETPRICE_API_KEY");
$etag = null;
$ch = curl_init("$base/sku-mappings/10072L");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer $key",
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HEADERFUNCTION =>
        function ($ch, $line) use (&$etag) {
            if (stripos($line, "ETag:") === 0) {
                $etag = trim(substr($line, 5));
            }
            return strlen($line);
        },
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($body === false || $status >= 400) {
    throw new RuntimeException(
        "GetPrice returned $status: $body"
    );
}
$data = json_decode(
    $body, true, flags: JSON_THROW_ON_ERROR
);

echo $data["sku"], " ", $data["product_id"], "\n";
echo "ETag: ", $etag, "\n";
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using Naming = System.Text.Json.JsonNamingPolicy;

var key = Environment.GetEnvironmentVariable(
    "GETPRICE_API_KEY");
var api = new Uri("https://api.getprice.ai/v1/");
var http = new HttpClient { BaseAddress = api };
http.Timeout = TimeSpan.FromSeconds(30);
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", key);
var json = new JsonSerializerOptions
{
    PropertyNamingPolicy = Naming.SnakeCaseLower,
};

var res = await http.GetAsync("sku-mappings/10072L");
res.EnsureSuccessStatusCode();
var mapping = await res.Content
    .ReadFromJsonAsync<SkuMapping>(json);
Console.WriteLine(
    $"{mapping!.Sku} {mapping.ProductId}");
Console.WriteLine($"ETag: {res.Headers.ETag?.Tag}");

record SkuMapping(string Sku, string? ProductId);
from urllib.parse import quote

import requests
from odoo import models

API = "https://api.getprice.ai/v1"


class ProductProduct(models.Model):
    _inherit = "product.product"

    def action_getprice_show_link(self):
        """Post what this reference is linked to."""
        self.ensure_one()
        icp = self.env["ir.config_parameter"].sudo()
        key = icp.get_param("getprice.api_key")
        resp = requests.get(
            f"{API}/sku-mappings/"
            + quote(self.default_code, safe=""),
            headers={"Authorization": f"Bearer {key}"},
            timeout=30,
        )
        if resp.status_code == 404:
            self.message_post(body="Not linked.")
            return
        resp.raise_for_status()
        m = resp.json()
        target = m["product_id"]
        target = target or m["comparison_set_id"]
        self.message_post(body=f"Linked to {target}")
Response
{
  "sku": "10072L",
  "product_id": "prd_8fK2mQa1",
  "comparison_set_id": null,
  "source": "list_sku",
  "created_at": "2026-11-02T10:04:12Z",
  "updated_at": "2026-11-02T10:04:12Z"
}

Path parameters

  • sku#stringRequired

    Your SKU, percent-encoded. SKUs may contain /, spaces, +, % and non-ASCII characters; encode every reserved character (e.g. AB/12 X → AB%2F12%20X). Surrounding whitespace (space, tab, CR, LF, non-breaking space) is trimmed. A SKU can't contain line breaks or other control characters inside it.

Headers

  • If-None-Match#string

    An ETag from a previous response. If nothing changed you get 304 Not Modified with no body.

Returns

200

The SKU mapping.

Returns the SKU mapping object.

Errors

  • 400

    The request is malformed: an invalid or unknown query parameter, a bad cursor, more than 100 sku values, or an invalid SKU in the path (empty after trimming or over 100 characters).

  • 401

    No key (unauthenticated), or a key that is malformed, unknown, revoked or expired (invalid_key; for security the response doesn't say which).

  • 403

    The key is valid but lacks the scope this endpoint needs.

  • 404

    Not found for your company.

  • 429

    This request costs more rate-limit units than you have left.

  • 500

    Something went wrong on our side.

  • 503

    Temporarily unavailable: GetPrice can't guarantee correct match data right now, or the database is saturated.

How errors are shaped

Requires skus:write

Links your SKU to one of your own products, creating or replacing the link. Returns 201 with a Location header for a new link, and 200 when it replaced one.

Sending the same request twice has the same effect as sending it once. Surrounding whitespace in the SKU is trimmed. A product id that isn't one of your own products returns 404 product_not_found.

To avoid overwriting someone else's link:

  • If-None-Match: * creates only. If the SKU is already linked, you get 412 and nothing changes.
  • If-Match: <ETag> replaces only if the link hasn't changed since you read it. Otherwise you get 412.
BASE=https://api.getprice.ai/v1
curl -X PUT "$BASE/sku-mappings/13817L" \
  -H "Authorization: Bearer $GETPRICE_API_KEY" \
  -H "If-None-Match: *" \
  -H "Content-Type: application/json" \
  -d '{"product_id": "prd_Lp4sT1x9"}'
import os

import requests

BASE = "https://api.getprice.ai/v1"
KEY = os.environ["GETPRICE_API_KEY"]

resp = requests.put(
    f"{BASE}/sku-mappings/13817L",
    json={"product_id": "prd_Lp4sT1x9"},
    headers={
        "Authorization": f"Bearer {KEY}",
        "If-None-Match": "*",
    },
    timeout=30,
)
resp.raise_for_status()
created = resp.status_code == 201
print("created" if created else "replaced")
const BASE = "https://api.getprice.ai/v1";
const KEY = process.env.GETPRICE_API_KEY;
const url = `${BASE}/sku-mappings/13817L`;
const res = await fetch(url, {
  method: "PUT",
  headers: {
    Authorization: `Bearer ${KEY}`,
    "Content-Type": "application/json",
    "If-None-Match": "*",
  },
  body: JSON.stringify({
    product_id: "prd_Lp4sT1x9",
  }),
});
if (!res.ok) {
  throw new Error(`GetPrice returned ${res.status}`);
}
const created = res.status === 201;
console.log(created ? "created" : "replaced");
<?php
$base = "https://api.getprice.ai/v1";
$key = getenv("GETPRICE_API_KEY");
$ch = curl_init("$base/sku-mappings/13817L");
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => "PUT",
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer $key",
        "Content-Type: application/json",
        "If-None-Match: *",
    ],
    CURLOPT_POSTFIELDS => json_encode([
        "product_id" => "prd_Lp4sT1x9",
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($body === false || $status >= 400) {
    throw new RuntimeException(
        "GetPrice returned $status: $body"
    );
}

echo $status === 201 ? "created" : "replaced", "\n";
using System.Net.Http.Headers;
using System.Net.Http.Json;

var key = Environment.GetEnvironmentVariable(
    "GETPRICE_API_KEY");
var api = new Uri("https://api.getprice.ai/v1/");
var http = new HttpClient { BaseAddress = api };
http.Timeout = TimeSpan.FromSeconds(30);
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", key);

var req = new HttpRequestMessage(
    HttpMethod.Put, "sku-mappings/13817L")
{
    Content = JsonContent.Create(
        new { product_id = "prd_Lp4sT1x9" }),
};
// Create only: 412 if the SKU is already linked.
req.Headers.IfNoneMatch.Add(EntityTagHeaderValue.Any);
var res = await http.SendAsync(req);
res.EnsureSuccessStatusCode();
var created = (int)res.StatusCode == 201;
Console.WriteLine(created ? "created" : "replaced");
from urllib.parse import quote

import requests
from odoo import models
from odoo.exceptions import UserError

API = "https://api.getprice.ai/v1"


class ProductProduct(models.Model):
    _inherit = "product.product"

    def getprice_link_sku(self, gp_product_id):
        """Link this reference; never overwrite."""
        self.ensure_one()
        icp = self.env["ir.config_parameter"].sudo()
        key = icp.get_param("getprice.api_key")
        resp = requests.put(
            f"{API}/sku-mappings/"
            + quote(self.default_code, safe=""),
            json={"product_id": gp_product_id},
            headers={
                "Authorization": f"Bearer {key}",
                "If-None-Match": "*",
            },
            timeout=30,
        )
        if resp.status_code == 412:
            raise UserError("Already linked.")
        resp.raise_for_status()
        self.message_post(
            body=f"Linked to {gp_product_id}"
        )
Response
{
  "sku": "10072L",
  "product_id": "prd_8fK2mQa1",
  "comparison_set_id": null,
  "source": "list_sku",
  "created_at": "2026-11-02T10:04:12Z",
  "updated_at": "2026-11-02T10:04:12Z"
}

Path parameters

Headers

Body parameters

Returns

200

An existing link was replaced (or re-sent unchanged).

Returns the SKU mapping object.

201

A new link was created.

Returns the SKU mapping object.

Errors

  • 400

    The request is malformed: an invalid or unknown query parameter, a bad cursor, more than 100 sku values, or an invalid SKU in the path (empty after trimming or over 100 characters).

  • 401

    No key (unauthenticated), or a key that is malformed, unknown, revoked or expired (invalid_key; for security the response doesn't say which).

  • 403

    The key is valid but lacks the scope this endpoint needs.

  • 404

    product_id is not one of your own products (for example a competitor's product, or an unknown id).

  • 412

    If-Match did not match, or If-None-Match: * was sent and the SKU is already linked.

  • 413

    Request body over 1 MB.

  • 415

    The body is not application/json.

  • 422

    The body is not valid: not JSON, missing product_id, or extra fields.

  • 429

    This request costs more rate-limit units than you have left.

  • 500

    Something went wrong on our side.

  • 503

    Temporarily unavailable: GetPrice can't guarantee correct match data right now, or the database is saturated.

How errors are shaped

Requires skus:write

Removes the link for your SKU and returns 204. Your product and its comparison set stay as they are. Returns 404 if the SKU isn't linked.

BASE=https://api.getprice.ai/v1
curl -X DELETE "$BASE/sku-mappings/13817L" \
  -H "Authorization: Bearer $GETPRICE_API_KEY"
import os

import requests

BASE = "https://api.getprice.ai/v1"
KEY = os.environ["GETPRICE_API_KEY"]

resp = requests.delete(
    f"{BASE}/sku-mappings/13817L",
    headers={"Authorization": f"Bearer {KEY}"},
    timeout=30,
)
resp.raise_for_status()
print("unlinked")
const BASE = "https://api.getprice.ai/v1";
const KEY = process.env.GETPRICE_API_KEY;
const url = `${BASE}/sku-mappings/13817L`;
const res = await fetch(url, {
  method: "DELETE",
  headers: { Authorization: `Bearer ${KEY}` },
});
if (!res.ok) {
  throw new Error(`GetPrice returned ${res.status}`);
}
console.log("unlinked");
<?php
$base = "https://api.getprice.ai/v1";
$key = getenv("GETPRICE_API_KEY");
$ch = curl_init("$base/sku-mappings/13817L");
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => "DELETE",
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer $key",
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($body === false || $status >= 400) {
    throw new RuntimeException(
        "GetPrice returned $status: $body"
    );
}

echo "unlinked\n";
using System.Net.Http.Headers;

var key = Environment.GetEnvironmentVariable(
    "GETPRICE_API_KEY");
var api = new Uri("https://api.getprice.ai/v1/");
var http = new HttpClient { BaseAddress = api };
http.Timeout = TimeSpan.FromSeconds(30);
http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", key);

var res = await http.DeleteAsync(
    "sku-mappings/13817L");
res.EnsureSuccessStatusCode();
Console.WriteLine("unlinked");
from urllib.parse import quote

import requests
from odoo import models

API = "https://api.getprice.ai/v1"


class ProductProduct(models.Model):
    _inherit = "product.product"

    def action_getprice_unlink_sku(self):
        """Remove this reference's GetPrice link."""
        self.ensure_one()
        icp = self.env["ir.config_parameter"].sudo()
        key = icp.get_param("getprice.api_key")
        resp = requests.delete(
            f"{API}/sku-mappings/"
            + quote(self.default_code, safe=""),
            headers={"Authorization": f"Bearer {key}"},
            timeout=30,
        )
        # 404 means it wasn't linked, which is fine.
        if resp.status_code != 404:
            resp.raise_for_status()
        self.message_post(body="Unlinked in GetPrice")
Response

No response body.

Path parameters

Headers

Returns

204

The link was removed.

Errors

  • 400

    The request is malformed: an invalid or unknown query parameter, a bad cursor, more than 100 sku values, or an invalid SKU in the path (empty after trimming or over 100 characters).

  • 401

    No key (unauthenticated), or a key that is malformed, unknown, revoked or expired (invalid_key; for security the response doesn't say which).

  • 403

    The key is valid but lacks the scope this endpoint needs.

  • 404

    The resource does not exist for your company.

  • 412

    If-Match did not match, or If-None-Match: * was sent and the SKU is already linked.

  • 429

    This request costs more rate-limit units than you have left.

  • 500

    Something went wrong on our side.

  • 503

    Temporarily unavailable: GetPrice can't guarantee correct match data right now, or the database is saturated.

How errors are shaped