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.
https://api.getprice.ai/v1Authentication#
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.
| Scope | Grants |
|---|---|
catalog:read | your products, competitors, SKU links |
matches:read | comparison sets (competitor matches and prices) |
skus:write | create, 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.
{
"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
| Status | Meaning | What to do |
|---|---|---|
| 200, 201, 204 | It worked | Read the body, if there is one |
| 304 | Nothing changed since your | Use your cached copy |
| 400 | The request is malformed | Fix the request; check |
| 401 | No key, or a key that doesn't work | Check the |
| 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 |
| 412 | A precondition failed | Read the resource again, then decide |
| 413 | The body is over 1 MB, whether or not you sent | Send less |
| 415 | The body isn't JSON | Send |
| 422 | The JSON body is invalid | Fix the field named in |
| 429 | Too many requests | Wait |
| 500 | Something failed on our side | Don't retry; contact support with the |
| 503 | Temporarily unavailable | Wait |
A 401 carries a WWW-Authenticate: Bearer realm="api.getprice.ai" header.
Attributes
type#stringDocumentation URL for this error;
codeis its slug.title#stringstatus#integerdetail#stringHuman-readable explanation for this occurrence.
code#stringStable 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_limitedandquota_exceededare reserved for later features.request_id#string- Example
- req_01JBX4Q8ZJ7M2K
errors#array of objectsField-level details for validation errors.
limit#stringOn 429, which limit was hit (e.g.
sustained).retry_after#integerOn 429, seconds to wait (same as the
Retry-Afterheader).
Error codes
| Code | Status | Meaning | How to fix |
|---|---|---|---|
validation_failed | 400 | A parameter value is invalid, such as more than 100 sku values or limit=0 | Fix the value named in errors. Don't retry. |
unknown_parameter | 400 | The endpoint doesn't take this query parameter, often because of a typo | Fix the parameter name. Don't retry. |
invalid_parameter | 400 | A parameter value contains a control character | Remove the character. Don't retry. |
cursor_mismatch | 400 | The filters or the endpoint changed while you paged with a cursor | Start again without a cursor. Don't retry. |
invalid_cursor | 400 | The cursor is malformed, altered or expired | Start again without a cursor. Don't retry. |
unauthenticated | 401 | There's no Authorization: Bearer header | Send your key. Don't retry. |
invalid_key | 401 | The key is malformed, unknown, revoked or expired. We don't say which | Check for copy-paste damage. If the key is intact, an admin can regenerate it in Settings. Don't retry. |
insufficient_scope | 403 | The key lacks this endpoint's scope | Use a key with that scope. Don't retry. |
not_found | 404 | There's no such resource for your company | Check the id or SKU. Don't retry. |
product_not_found | 404 | product_id isn't one of your own products | Use an id from GET /v1/products. Don't retry. |
method_not_allowed | 405 | The path doesn't support this HTTP method | Use a method from the Allow header. Don't retry. |
precondition_failed | 412 | You sent If-None-Match: * and the SKU is already linked, or If-Match didn't match | Read the link again, then decide. Don't retry. |
payload_too_large | 413 | The request body is over 1 MB, whether or not you sent Content-Length | Send a smaller body. Don't retry. |
unsupported_media_type | 415 | The body isn't application/json | Set Content-Type: application/json. Don't retry. |
validation_failed | 422 | The JSON body is invalid, such as a missing product_id | Fix the field named in errors. Don't retry. |
rate_limited | 429 | This request costs more units than you have left this minute | Wait Retry-After seconds, then retry |
internal_error | 500 | Something failed on our side | Contact support with the request_id. Don't retry. |
unavailable | 503 | GetPrice can't guarantee correct match data right now, or is too busy | Wait 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",
},
}{
"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#stringAn ETag from a previous response. If nothing changed you get
304 Not Modifiedwith no body.
Returns
The calling key.
Errors
- 400
The request is malformed: an invalid or unknown query parameter, a bad cursor, more than 100
skuvalues, 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.
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#stringOpaque comparison set id.
- Example
- set_x7Kp2Lm9
name#stringYour SKU for this set, or
nullif none is linked.Your product in this set, or
nullif the set has no product of yours.Show child attributesHide child attributes
id#stringOpaque product id. Stable across URL changes and re-collection.
- Example
- prd_8fK2mQa1
name#stringurl#stringYour current price, or
nullif none has been collected.Show child attributesHide child attributes
amount#stringThe price a shopper pays now. During a promotion, this is the discounted price. "Cheapest" and "position" in GetPrice always use
amount.- Example
- 18.99
The original (pre-discount) price the retailer shows during a promotion, from the same price reading as
amount. Sent only when strictly greater thanamount; otherwisenull(not on promotion).- Example
- 18.99
currency#stringISO 4217 code. Always present.
- Example
- ZAR
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".
nullwhenpriceisnull.- Example
- 2026-11-02T04:12:09Z
stale#booleantruewhenlast_collected_atis 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 objectsEvery competitor in the set, as the dashboard shows it.
[]when there are none.Show child attributesHide child attributes
competitor#objectproduct#objectThe competitor's current price, or
nullif no valid price is available.Show child attributesHide child attributes
amount#stringThe price a shopper pays now. During a promotion, this is the discounted price. "Cheapest" and "position" in GetPrice always use
amount.- Example
- 18.99
The original (pre-discount) price the retailer shows during a promotion, from the same price reading as
amount. Sent only when strictly greater thanamount; otherwisenull(not on promotion).- Example
- 18.99
currency#stringISO 4217 code. Always present.
- Example
- ZAR
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".
nullwhenpriceisnull.- Example
- 2026-11-02T04:12:09Z
availability#stringStock status on the competitor's site. GetPrice does not collect stock yet, so this is
unknownfor 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#stringactivewhile the product is listed on the competitor's site.delistedproducts are only returned withinclude_delisted=true.- One of (more may be added)
- active, delisted
stale#booleantruewhenlast_collected_atis 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#objectHow this competitor product came to be in the set.
Show child attributesHide child attributes
review#stringsame— 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
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
The matcher's confidence from 0 to 1 when a machine match backs this entry, otherwise
null.- Range
- 0 to 1
integrity#stringok— 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#timestampWhen 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
skufor up to 100 of your SKUs:?sku=10072L&sku=11247P.dataholds a set for each SKU that has one, andskusgives each SKU's status:found,no_comparison_setorunknown_sku. - Sync everything. Leave out
skuand follownext_cursoruntil it isnull.
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}"
){
"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 stringsYour 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:00981Aand981Aare different SKUs.- Max items
- 100
reviewed_by#stringcustomerreturns only competitors your own users have confirmed. Omit for every competitor in the set.- One of
- customer
include_delisted#booleantruealso returns competitor products that are no longer listed (status: delisted).- Default
- false
limit#integerItems per page. Default 100. Values above 500 are treated as 500.
- Default
- 100
cursor#stringThe
next_cursorfrom the previous page. Opaque — pass it back unchanged, together with the same filters, on the same endpoint, as the first request (otherwise 400cursor_mismatch).
Headers
If-None-Match#stringAn ETag from a previous response. If nothing changed you get
304 Not Modifiedwith no body.
Returns
A page of comparison sets.
Pass as
cursorto get the next page.nullon the last page.skus#array of objectsOnly 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#stringThe SKU as you sent it (trimmed).
- Example
- 10072L
status#stringfound— linked to your product, which has a comparison set (indata).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 withPUT /v1/sku-mappings/{sku}. Open set.- One of (more may be added)
- found, no_comparison_set, unknown_sku
Opaque comparison set id.
- Example
- set_x7Kp2Lm9
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
skuvalues, 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.
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)){
"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
The set id, e.g.
set_x7Kp2Lm9.- Example
- set_x7Kp2Lm9
Query parameters
Headers
If-None-Match#stringAn ETag from a previous response. If nothing changed you get
304 Not Modifiedwith no body.
Returns
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
skuvalues, 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.
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#stringOpaque product id. Stable across URL changes and re-collection.
- Example
- prd_8fK2mQa1
name#stringurl#stringskus#array of stringsEvery SKU you have linked to this product.
[]when none.The comparison set this product sits in, or
nullif it is not in one yet.- Example
- set_x7Kp2Lm9
A price as read from a website. When it was read is the sibling
last_collected_at.Show child attributesHide child attributes
amount#stringThe price a shopper pays now. During a promotion, this is the discounted price. "Cheapest" and "position" in GetPrice always use
amount.- Example
- 18.99
The original (pre-discount) price the retailer shows during a promotion, from the same price reading as
amount. Sent only when strictly greater thanamount; otherwisenull(not on promotion).- Example
- 18.99
currency#stringISO 4217 code. Always present.
- Example
- ZAR
RFC 3339, UTC,
Zsuffix.- Example
- 2026-11-02T04:12:09Z
status#stringactive, ordelistedwhen the product is no longer on your site (only withinclude_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}"){
"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 stringsYour 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:00981Aand981Aare different SKUs.- Max items
- 100
q#stringCase-insensitive substring of the product name.
unmapped#booleantruereturns only products with no SKU linked.- Default
- false
include_delisted#booleantruealso returns products no longer on your site (status: delisted).- Default
- false
limit#integerItems per page. Default 100. Values above 500 are treated as 500.
- Default
- 100
cursor#stringThe
next_cursorfrom the previous page. Opaque — pass it back unchanged, together with the same filters, on the same endpoint, as the first request (otherwise 400cursor_mismatch).
Headers
If-None-Match#stringAn ETag from a previous response. If nothing changed you get
304 Not Modifiedwith no body.
Returns
A page of products.
Pass as
cursorto get the next page.nullon the last page.
Errors
- 400
The request is malformed: an invalid or unknown query parameter, a bad cursor, more than 100
skuvalues, 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.
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']}"
){
"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
The product id, e.g.
prd_8fK2mQa1.- Example
- prd_8fK2mQa1
Headers
If-None-Match#stringAn ETag from a previous response. If nothing changed you get
304 Not Modifiedwith no body.
Returns
The product.
Returns the product object.
Errors
- 400
The request is malformed: an invalid or unknown query parameter, a bad cursor, more than 100
skuvalues, 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.
The competitor object#
{
"id": "cmp_3LqV8nP2",
"name": "Harbour Market",
"domain": "harbourmarket.example.com",
"url": "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,
}){
"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
Headers
If-None-Match#stringAn ETag from a previous response. If nothing changed you get
304 Not Modifiedwith no body.
Returns
A page of competitors.
Pass as
cursorto get the next page.nullon the last page.
Errors
- 400
The request is malformed: an invalid or unknown query parameter, a bad cursor, more than 100
skuvalues, 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.
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.
{
"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
The linked product, or
nullwhen the SKU is linked to a comparison set instead.- Example
- prd_8fK2mQa1
The linked comparison set when it has no product of yours (set up by GetPrice), otherwise
null. APUTto such a SKU replaces the link with a product link.- Example
- set_x7Kp2Lm9
source#stringHow 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#timestampRFC 3339, UTC,
Zsuffix.- Example
- 2026-11-02T04:12:09Z
updated_at#timestampRFC 3339, UTC,
Zsuffix.- Example
- 2026-11-02T04:12:09Z
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{
"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
Headers
If-None-Match#stringAn ETag from a previous response. If nothing changed you get
304 Not Modifiedwith no body.
Returns
A page of SKU mappings.
Pass as
cursorto get the next page.nullon the last page.
Errors
- 400
The request is malformed: an invalid or unknown query parameter, a bad cursor, more than 100
skuvalues, 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.
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}"){
"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
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#stringAn ETag from a previous response. If nothing changed you get
304 Not Modifiedwith no body.
Returns
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
skuvalues, 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.
Link a SKU to a product#
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}"
){
"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
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
Body parameters
One of your own products (from
GET /v1/products).- Example
- prd_8fK2mQa1
Returns
An existing link was replaced (or re-sent unchanged).
Returns the SKU mapping object.
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
skuvalues, 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_idis not one of your own products (for example a competitor's product, or an unknown id). - 412
If-Matchdid not match, orIf-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.
Remove a SKU link#
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")No response body.
Path parameters
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-Match#stringThe ETag from a previous
GET; delete only if unchanged.
Returns
The link was removed.
Errors
- 400
The request is malformed: an invalid or unknown query parameter, a bad cursor, more than 100
skuvalues, 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-Matchdid not match, orIf-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.