Safe retriesEvery mutating POST requires an idempotency key
Quickstart
1. Create your API key
Sign in and open the workspace whose data you want to use.
Open Developers → API keys. You need the owner, admin, or developer workspace role.
Enter a key name, select Product integration, choose an expiry, then select Create API key.
Save the full secret now; it is shown only once. This preset includes products:resolve, products:read, and changes:read.
Run the following blocks in the same Bash terminal (Git Bash on Windows), with Node.js 22+ and curl 7.76+ installed. The base URL below uses this website's origin, without a trailing slash or /v1. Use the origin that issued your key if it differs.
2. Set your credentials and target
SETUP · BASH
export BASE_URL='https://YOUR_MONITO_HOST'
# Paste the full issued key when prompted (input is hidden).
read -r -s -p 'Monito API key: ' MONITO_API_KEY; printf '\n'
export MONITO_API_KEY
# Enter an actual public retail product URL you want to extract.
read -r -p 'Public product URL: ' TARGET_URL
export TARGET_URL
# Generate once per new resolution. Reuse this value for a retry.
RESOLVE_KEY="$(node -e 'console.log(crypto.randomUUID())')"
Enter a real, publicly accessible retail product URL. An example.com placeholder is not a working product. Successful extraction depends on the page's content and access policies; a supported resource type does not guarantee support for every website.
3. Start a resolution
REQUEST · POST /v1/products/resolve
# Requires Bash (Git Bash on Windows), curl, and Node.js 22+.
# Run the setup block first. Keep this key for retries of this exact request.
RESOLVE_RESPONSE=$(curl -sS --fail-with-body -X POST "$BASE_URL/v1/products/resolve" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $MONITO_API_KEY" \
-H "Idempotency-Key: $RESOLVE_KEY" \
-d "$(node -e 'process.stdout.write(JSON.stringify({url:process.env.TARGET_URL,resourceType:"retail_product",mode:"async"}))')")
printf '%s\n' "$RESOLVE_RESPONSE"
The response above is illustrative. Your IDs will differ. A 202 means the request was accepted, not that extraction succeeded. If the response contains error, follow Errors before continuing.
4. Poll until the operation finishes
Resolution is asynchronous. Poll the returned statusUrl with the same bearer key. While data.status is pending, repeat the poll block with a delay; for example, start at two seconds and increase to ten seconds. This is client guidance, not a completion-time guarantee. Set an overall timeout in your integration.
POLL
# Use the real statusUrl returned above, after a successful 202 response.
STATUS_URL=$(printf '%s' "$RESOLVE_RESPONSE" | node -e 'const b=JSON.parse(require("node:fs").readFileSync(0,"utf8")); if(!b.data?.statusUrl) throw Error("Resolve was not accepted; inspect the response above."); process.stdout.write(b.data.statusUrl);')
if [ -n "$STATUS_URL" ]; then
OPERATION_RESPONSE=$(curl -sS --fail-with-body "$BASE_URL$STATUS_URL" \
-H "Authorization: Bearer $MONITO_API_KEY")
printf '%s\n' "$OPERATION_RESPONSE"
fi
Polling returns HTTP 200 even when data.status is failed. Stop on succeeded or failed. The operation response does not contain a detailed failure reason. For checks on a watch, inspect its /runs endpoint. A timeout in your client does not cancel the operation; keep its statusUrl to resume polling.
5. Read the product
READ · GET /v1/products/{id}
# Run only after polling reports data.status = "succeeded".
PRODUCT_ID=$(printf '%s' "$OPERATION_RESPONSE" | node -e 'const b=JSON.parse(require("node:fs").readFileSync(0,"utf8")); if(b.data?.status!=="succeeded" || !b.data.productId) throw Error("A succeeded resolution with a productId is required."); process.stdout.write(b.data.productId);')
if [ -n "$PRODUCT_ID" ]; then
curl -sS --fail-with-body "$BASE_URL/v1/products/$PRODUCT_ID" \
-H "Authorization: Bearer $MONITO_API_KEY"
fi
Use data.productId from the succeeded operation. An ID in the initial accepted response alone is not proof that normalized data is ready. Resolving a product is a one-time extraction; create a watch for recurring monitoring.
Using the local demo?
Browser-created demo keys are UI placeholders and cannot authenticate REST requests. The fixture server uses a separately configured test key; with live extraction disabled, new resolutions remain pending. Use a connected deployment and its issued key to complete this guide.
Authentication
Send a workspace-bound API key in the standard bearer format. Keys are scoped: a scope grants permission for an API action. If an endpoint lists multiple scopes, any one is sufficient. Normal key issuance offers only the listed scopes; admin:* is reserved for internally provisioned administrative or legacy keys.
HTTP HEADER
Authorization: Bearer $MONITO_API_KEY
Signing in to the website does not authenticate REST requests. The bearer key selects the workspace; no workspace ID or slug is added to API paths. IDs from another workspace return 404. Keep keys on your backend, out of browser code, public repositories, URLs, and logs. CORS does not make a browser-held secret safe.
If the key expires or is revoked, create a replacement in Developers → API keys and update your integration. Lost secrets cannot be read back. For additional actions, enable Customize scopes (advanced) when creating the key and select only the scopes you need.
Successful JSON responses use {data: ...}. Collection responses use {data: [...], page: {nextCursor, hasMore}}. A null nextCursor means there are no further pages. Send limit from 1–100 (default 50) and pass the opaque cursor back unchanged as the URL-encoded cursor query parameter, keeping the same filters. A page may contain fewer items than the limit, or be empty, while hasMore is true. Do not stop solely because data is empty.
Non-empty write bodies require application/json and are limited to 1 MiB; larger payloads return 413. Unknown top-level JSON request fields are rejected. Successful DELETE requests return 204 with no body; do not try to parse JSON from a 204 response.
Query timestamps from and to are inclusive Unix milliseconds. Response fields named createdAt, updatedAt, startedAt, and finishedAt are Unix milliseconds. Fields such as observedAt, detectedAt, and nextRunAtare ISO 8601 strings. Other watch timestamps such as lastScheduledAtare numeric milliseconds; follow each field's schema.
Read requests are limited to 120 per minute and writes to 30 per minute per workspace and route group; all keys in a connected workspace share these allowances. Responses include X-Request-Id; once rate limiting is evaluated, they also include RateLimit-Remaining and RateLimit-Reset (Unix seconds). Route-group rate limits return 429 with Retry-After; the same header is present for a timed manual-check cooldown and monthly API-request exhaustion. For that monthly API quota, RateLimit-Reset and Retry-Afterpoint to the next UTC month. Other plan, monthly, or capacity quotas can also return 429 without a recovery time. Check the error message: increase the requested interval, free capacity, change plan, or wait for a monthly allowance to reset as appropriate. Repeating an unchanged invalid plan request will not help. An idempotent replay includes Idempotency-Replayed: true.
Endpoint reference
22 of 22 operations match your search. POST operations marked idempotent require Idempotency-Key with 8–255 characters. Keys are bound to the API key and endpoint for 24 hours; reusing one with a different body returns a conflict. Generate a new key for each new action; reuse the same key, API key, endpoint, and exact serialized JSON body when retrying that action. After 24 hours, reusing a key may execute a new action. Changing whitespace or JSON property order can cause a conflict. PATCH and DELETE do not use this POST replay mechanism.
POST
/v1/products/resolve
Start an async public-URL resolution.
Send a public URL and poll the returned operation with the same products:resolve key.
JSON: url required; optional resourceType, localeHint, clientReference, metadata, and manualExtraction. mode is async only.
Returns plan metrics for checks, rendering, API requests, active watches, and webhook endpoints.
No parameters.
Scope: usage:read · Success: 200 · UsageResponse
GET
/v1/health
Check service health; authentication is not required.
Returns the API service status and UTC timestamp without a bearer key.
No parameters or bearer key.
Scope: public · Success: 200 · HealthResponse
Products
A product is a normalized record of a public page, including non-retail resources. Resolve requires url; optional fields are resourceType, mode (only async), localeHint (a BCP 47 tag such as el-GR), clientReference (up to 200 characters), metadata (up to 50 string-valued entries), and manualExtraction.
POST /v1/products/resolve accepts resource types retail_product, property_listing, travel_fare, ferry_fare, public_tender, saas_plan, industrial_part, and generic_price_page. Optional manualExtraction has version 1, 1–32 fields, safe CSS selectors, and a parser matching the selected monitorable path.
GET /v1/products/{id} returns available normalized fields, confidence, and a history link. Price, stock, shipping, and resource-specific fields depend on what could be extracted. Treat absent values as unknown, not zero. Money uses integer minor units plus currency: {"amountMinor":12999,"currency":"EUR"}means €129.99; use the currency's minor-unit precision rather than always dividing by 100. History defaults to changed observations; use include=no_change_runs for every observation and fields=price.amountMinor,stockStatus to narrow changed fields. History filters match exact changed paths, not parent prefixes, and do not remove fields from the observation object.
Monitorable fields by resource type
Use these paths in monitor.fields when creating a watch or monitoredFields when updating it. A parent path such as price monitors its children. Resource-specific fields may be absent if the page does not expose them.
A watch schedules repeated checks on a product. Create it with POST /v1/watches, a key with watches:write, and a new Idempotency-Key. The Product integration preset alone cannot create watches; customize the scopes of a new key or use a separate Watch management and checks key.
Replace the product ID with the result from the quickstart. This body contains all required fields for a retail product watch and disables email and webhook notifications. To enable webhook notifications, create an endpoint first and include its ID in notifications.webhookEndpointIds. The endpoint must be active and subscribed to the events you want. Setting notifications.email to true requires configured workspace email recipients; otherwise creation returns 422. This request does not accept recipient addresses.
Create with exactly one of productId or url. A URL may return 202 while resolution is pending; that response includes a full watch plus operationId and statusUrl. The minimumschedule.intervalMinutes is 60 for trial, 15 for starter, 5 for pro, and 1 for enterprise; a shorter cadence returns 429 QUOTA_EXCEEDED. On creation, schedule.timezone must be an IANA name, such as Europe/Athens. This is an interval schedule, not a daily clock-time or cron schedule. The interval does not guarantee an exact extraction completion time.
Save the watch's data.id. Use GET /v1/watches/{id} with watches:read to inspect it. PATCH uses different field names from creation: timezone, monitoredFields, and rules are top-level fields; the interval stays in schedule.intervalMinutes. Notifications and the target cannot be changed through this PATCH endpoint.
JSON BODY · PATCH /v1/watches/{id}
{"status":"paused"}
DELETE archives the watch. POST /v1/watches/{id}/checks requires watches:check and an idempotency key, accepts no body or {}, and returns an operation to poll. A manual check is queued asynchronously and can be rejected by a cooldown, an existing run, or a quota.
Changes
Changes expose stable IDs, event IDs, product/watch IDs, ISO detectedAt, summary, and field entries with before/after values, monetary deltas, severity, and optional provenance. Filter with watchId, productId, clientReference, type, field, from, and to. If both watchId and clientReferenceare supplied, watchId takes precedence. Keep watch references unique; an ambiguous clientReference returns 409.
The first successful observation establishes a baseline; a missing change does not mean a check failed. Failed extraction retains the last good data. Changes are recorded for monitored fields; notification rules can suppress an alert without removing the change from the feed.
Webhooks
Create an HTTPS endpoint and store the returned secret. Subscribe to product.resolved, watch.created, change.detected, check.failed, watch.degraded, and watch.recovered. The exact envelope, payload families, HMAC verification example, retries, 410 handling, and deduplication rules are in the Webhook event guide and the OpenAPI component schemas.
The signing secret is different from your API key. It is absent from later endpoint reads, but an exact idempotent replay of creation returns the original secret during the 24-hour replay window. Verify webhook signatures against the raw body before processing events.
Test deliveries can be queued only for active endpoints. Secret rotation is currently available in the connected Integrations UI and takes effect immediately; there is no dual-secret grace period.
Deliveries are at-least-once. Deduplicate by the envelope id; replay keeps that event ID while creating a new delivery ID. Any 2xx succeeds; 410 disables an endpoint. Other failures retry on the documented schedule and can become terminal failed deliveries. A successful 2xx resets the endpoint counter, so automatic disabling occurs after 20 consecutive failed attempts, not lifetime failures.
Usage & health
GET /v1/usage returns the current UTC month (YYYY-MM) and used/limit pairs for checks, rendered checks, API requests, active watches, and webhook endpoints. GET /v1/health is public and returns {data: {status: "ok", service: "price-monitor-api", time}}. This is an API liveness check; deployment readiness is exposed separately by the application health route.
Errors
Errors use one stable envelope. details is an array of safe validation context, requestId is the correlation ID, retryable tells clients whether retrying is appropriate, and docsUrl points to one of the anchors below.
Check the HTTP status and error.code before reading data. For retryable failures, use bounded retries with an increasing delay and honor Retry-After in seconds when present. Keep the original idempotency key and body for POST retries. Correct authentication, permissions, or invalid input before retrying those errors. Save requestId for troubleshooting; never log the bearer key.