Developers
Read-only access to your store's orders, customers, products, and categories. Sync your data with spreadsheets, dashboards, ERPs, and automation tools through a stable, versioned REST API.
The Store Data API gives you programmatic, read-only access to your store's data: orders, customers, products, and categories. Use it to sync your store with spreadsheets, BI dashboards, ERPs, accounting tools, or automation platforms like Zapier and Make.
All endpoints are GET, return JSON, and live under a stable versioned prefix:
Building with an AI assistant? The whole reference is available as plain markdown at rovela.ai/developers.md and as an OpenAPI 3.1 spec at rovela.ai/api/v1/openapi.json. Paste either into your assistant and ask it to write your integration.
From zero to your first JSON response in about two minutes.
Open your dashboard, pick a store, and go to Settings → API → Create key. The full key (it starts with rvk_) is shown exactly once, so copy it somewhere safe. Each key belongs to one store.
When meta.has_more is true, pass meta.next_cursor back to get the next page. See Pagination for the full walk.
Every request needs your API key in the x-api-key header. Keys are scoped to exactly one store, which is why no store identifier appears in any URL.
A store can have up to 5 active keys, managed in Settings → API. Only a hash of each key is stored, so a lost key cannot be recovered, only replaced. Revoking a key takes effect on the very next request.
Keep keys on your server. The API deliberately sends no CORS headers, so browser calls fail by design. A key embedded in a website or mobile app would be readable by anyone, and it grants access to your customer data.
Every endpoint uses the same envelope. List endpoints return an array plus pagination metadata; detail endpoints return a single object; errors return a code and a human-readable message.
| Convention | Detail |
|---|---|
| Field names | snake_case, matching the store database. |
| Money | Strings with 2 decimals, for example "49.90", never floats. The store currency is on /api/v1/store. |
| Timestamps | ISO 8601 in UTC, for example "2026-06-14T09:21:33.000Z". |
| Ids | UUIDs, unique within a store. |
| Guest checkout | orders.customer_id is null for guest orders; orders.email is always present. |
| Forward compatibility | New fields may appear at any time. Write clients that ignore fields they do not recognize. |
List endpoints are cursor-paginated, which stays correct even while orders are being written mid-export (offset pagination would skip or repeat rows). Rows come back oldest first by creation time. Cursors are opaque: pass them back exactly as received and never parse them.
| Parameter | Description |
|---|---|
| limit | Page size, 1 to 250. Defaults to 50. Use 250 for bulk exports. |
| cursor | The meta.next_cursor value from the previous page. |
For a recurring sync (hourly, nightly) you do not need full exports. Orders and products support updated_since; customers support created_since (customer records carry no updated timestamp).
last_synced_at timestamp taken before the run starts, then walk every page.id.last_synced_at to the new run's start time.Taking the timestamp before the run (not after) means an order updated while the run was in flight is picked up again next time. Duplicate upserts by id are harmless; missed rows are not.
Each key allows 120 requests per minute. Above that the API returns 429 RATE_LIMITED with a Retry-After header (seconds). Treat the limit as a target rather than a contract: it can be slightly more generous under horizontal scaling.
A full export of a large store fits comfortably inside the limit: 120 pages of 250 rows moves 30,000 rows per minute. If you hit 429s, request bigger pages instead of retrying faster.
Every error is JSON with a stable machine code and a human message. The codes below are the complete catalog, with what each one means and whether a retry can succeed.
| Status | Code | When it happens | What to do | Retry? |
|---|---|---|---|---|
| 401 | INVALID_API_KEY | The x-api-key header is missing, malformed, or the key was revoked. Revocation takes effect on the very next request. | Check that the header name is exactly x-api-key and the value starts with rvk_. If the key was revoked, create a new one in your store dashboard under Settings then API. | No |
| 403 | STORE_NOT_SUPPORTED | The key belongs to an imported store. Imported stores run on their own infrastructure, so their data does not live on Rovela. | Query the store on its own hosting instead. The Store Data API only serves stores generated on Rovela. | No |
| 409 | STORE_NOT_READY | The store exists but its database has not been provisioned yet, which only happens while the initial generation is still running. | Wait for the store to finish generating, then retry. | Yes |
| 400 | INVALID_PARAM | A query parameter failed validation: a limit outside 1 to 250, an unknown status value, a malformed datetime, or a non-UUID resource id. | The error message names the offending parameter and the accepted values. Correct the request. | No |
| 400 | INVALID_CURSOR | The cursor value could not be decoded. Cursors are opaque and only valid when passed back exactly as received. | Pass meta.next_cursor back verbatim. Never parse, edit, or store cursors long term; restart the walk from the first page if one goes stale. | No |
| 404 | NOT_FOUND | The resource id is a valid UUID but no matching row exists in this store. | Confirm the id belongs to this store. Ids are never shared across stores. | No |
| 429 | RATE_LIMITED | The key exceeded 120 requests in the current minute. | Honor the Retry-After header (seconds) before retrying. For bulk exports, request limit=250 pages instead of many small pages. | With backoff |
| 502 | STORE_DB_ERROR | A transient failure while reading the store database. | Retry with exponential backoff. If it persists for more than a few minutes, contact support. | With backoff |
The /api/v1 prefix is stable. Additive changes (new fields, endpoints, or filters) land in v1 without notice, which is why clients should ignore unknown fields. A breaking change would ship as /api/v2 with a migration window, never as an in-place change to v1.
A snapshot of the store the key belongs to: identity, currency, configuration flags, and row counts per resource. Use it as a connection sanity check and to learn the currency your money fields are denominated in.
| Field | Type | Description |
|---|---|---|
| id | uuid | The store id on Rovela. |
| name | string | The store name on the Rovela dashboard. |
| store_name | string or null | The customer-facing store name. |
| store_email | string or null | The store contact email. |
| store_currency | string or null | ISO 4217 currency code all money fields use, for example "USD". |
| store_timezone | string or null | IANA timezone, for example "America/New_York". |
| tax_included_in_prices | boolean or null | Whether displayed prices already include tax. |
| shipping_enabled | boolean or null | Whether the store ships physical goods. |
| guest_checkout | boolean or null | Whether customers can buy without an account. |
| customer_accounts | boolean or null | Whether customer accounts are enabled. |
| counts | object | Row counts: products, categories, customers, orders. |
GET/api/v1/store
Returns the store snapshot for the key you authenticate with. If this call works, every other endpoint will too.
Endpoint errors: STORE_NOT_SUPPORTED · STORE_NOT_READY
Orders placed on the storefront, with their line items embedded. Money fields are strings in the store currency. Guest orders have a null customer_id but always carry the buyer's email.
| Field | Type | Description |
|---|---|---|
| id | uuid | Order id. |
| customer_id | uuid or null | The buyer's customer id, or null for guest checkout. |
| string | The buyer's email. Always present. | |
| status | string | One of pending, paid, shipped, delivered, cancelled, refunded, return_requested. |
| subtotal | decimal string | Item total before tax, shipping, and discounts. |
| tax | decimal string | Tax charged. |
| shipping | decimal string | Shipping charged. |
| total | decimal string | Amount the customer paid. |
| refund_amount | decimal string or null | Total refunded so far. |
| return_reason | string or null | The customer's reason when a return was requested. |
| discount_code | string or null | The promotion code the customer redeemed, if any. |
| discount_amount | decimal string or null | Amount the discount removed. |
| shipping_address | object or null | firstName, lastName, line1, line2, city, state, postalCode, country, phone. |
| billing_address | object or null | Same shape as shipping_address. |
| stripe_payment_intent_id | string or null | Stripe PaymentIntent id, for reconciliation. |
| stripe_charge_id | string or null | Stripe Charge id. |
| receipt_url | string or null | Stripe-hosted receipt for the customer. |
| tracking_number | string or null | Shipment tracking number once fulfilled. |
| tracking_url | string or null | Carrier tracking link. |
| carrier | string or null | Carrier code, for example "ups". |
| shipping_method | string or null | The shipping option the customer chose. |
| created_at | timestamp | When the order was placed. |
| updated_at | timestamp | Last change to the order. Drives updated_since. |
| items | array | The order's line items, embedded. See Order item below. |
Order item
One line of an order. Name and price are snapshots taken at purchase time, so they stay correct even after the product changes.
| Field | Type | Description |
|---|---|---|
| id | uuid | Line item id. |
| order_id | uuid | The parent order. |
| product_id | uuid | The product purchased. |
| variant_id | uuid or null | The variant purchased, if the product has variants. |
| name | string | Product name at purchase time. |
| price | decimal string | Unit price at purchase time. |
| quantity | integer | Units purchased. |
| attributes | object or null | Variant attributes at purchase time, for example {"color": "Sand"}. |
| created_at | timestamp | When the line was written. |
GET/api/v1/orders
Returns orders oldest first, line items embedded. Filter by status or by updated_since for incremental syncs.
Query parameters
| Parameter | Type | Description |
|---|---|---|
| limit | integer | Page size, 1 to 250. Defaults to 50. |
| cursor | string | Opaque cursor from the previous page's meta.next_cursor. |
| updated_since | ISO 8601 datetime | Only orders updated at or after this instant. |
| status | string | One of pending, paid, shipped, delivered, cancelled, refunded, return_requested. |
Endpoint errors: INVALID_PARAM · INVALID_CURSOR
GET/api/v1/orders/{id}
Returns a single order with its line items.
Path parameters
| Parameter | Type | Description |
|---|---|---|
| id * | uuid | The order id. |
Endpoint errors: INVALID_PARAM · NOT_FOUND
Customers who created an account or checked out on the storefront. Passwords are never exposed through the API, on any endpoint.
| Field | Type | Description |
|---|---|---|
| id | uuid | Customer id. |
| string | The customer's email. Unique within the store. | |
| name | string or null | The customer's name, when provided. |
| stripe_customer_id | string or null | Stripe Customer id, for reconciliation. |
| email_verified | timestamp or null | When the email was verified, or null if it never was. |
| created_at | timestamp | When the customer record was created. Drives created_since. |
GET/api/v1/customers
Returns customers oldest first. Customer records are effectively append-only, so created_since gives you a clean incremental sync.
Query parameters
| Parameter | Type | Description |
|---|---|---|
| limit | integer | Page size, 1 to 250. Defaults to 50. |
| cursor | string | Opaque cursor from the previous page's meta.next_cursor. |
| created_since | ISO 8601 datetime | Only customers created at or after this instant. |
Endpoint errors: INVALID_PARAM · INVALID_CURSOR
GET/api/v1/customers/{id}
Returns a single customer.
Path parameters
| Parameter | Type | Description |
|---|---|---|
| id * | uuid | The customer id. |
Endpoint errors: INVALID_PARAM · NOT_FOUND
The store catalog, with variants embedded on every product. Products without variants return an empty variants array; their price and inventory live on the product itself.
| Field | Type | Description |
|---|---|---|
| id | uuid | Product id. |
| name | string | Product name. |
| slug | string | URL slug, unique within the store. |
| description | string | Product description. |
| price | decimal string | Base price. Variants can override it. |
| compare_price | decimal string or null | The crossed-out "was" price, when on sale. |
| images | string[] | Image URLs, in display order. |
| videos | string[] | Video URLs. |
| status | string | One of draft, active, archived. |
| category_id | uuid or null | The category the product belongs to. |
| has_variants | boolean | Whether the product sells in variants. |
| sku | string or null | Stock keeping unit, when set. |
| inventory | integer or null | Units in stock (for products without variants). |
| track_inventory | boolean or null | Whether stock is tracked. |
| continue_selling_when_out_of_stock | boolean or null | Whether checkout stays open at zero stock. |
| product_type | string or null | Free-form type label. |
| vendor | string or null | Brand or supplier name. |
| tags | string[] or null | Free-form tags. |
| meta_title | string or null | SEO title override. |
| meta_description | string or null | SEO description override. |
| meta_keywords | string[] or null | SEO keywords. |
| metafields | array | Custom fields as {key, label, value} objects. |
| created_at | timestamp | When the product was created. |
| updated_at | timestamp | Last change. Drives updated_since. |
| variants | array | The product's variants, embedded. See Variant below. |
Variant
One sellable variation of a product, for example a size and color combination. A null price means the variant sells at the product's base price.
| Field | Type | Description |
|---|---|---|
| id | uuid | Variant id. |
| product_id | uuid | The parent product. |
| sku | string | Stock keeping unit, unique within the store. |
| name | string | Variant display name, for example "Sand / Large". |
| price | decimal string or null | Price override, or null to use the product price. |
| inventory | integer | Units in stock for this variant. |
| image | string or null | Variant-specific image URL. |
| video | string or null | Variant-specific video URL. |
| attributes | object | The option values, for example {"color": "Sand", "size": "Large"}. |
| created_at | timestamp | When the variant was created. |
GET/api/v1/products
Returns products oldest first, variants embedded. Filter by status to export only the live catalog, or by updated_since for incremental syncs.
Query parameters
| Parameter | Type | Description |
|---|---|---|
| limit | integer | Page size, 1 to 250. Defaults to 50. |
| cursor | string | Opaque cursor from the previous page's meta.next_cursor. |
| updated_since | ISO 8601 datetime | Only products updated at or after this instant. |
| status | string | One of draft, active, archived. |
Endpoint errors: INVALID_PARAM · INVALID_CURSOR
GET/api/v1/products/{id}
Returns a single product with its variants.
Path parameters
| Parameter | Type | Description |
|---|---|---|
| id * | uuid | The product id. |
Endpoint errors: INVALID_PARAM · NOT_FOUND
The store's category tree. Categories can nest one level via parent_id; a null parent_id marks a top-level category.
| Field | Type | Description |
|---|---|---|
| id | uuid | Category id. |
| name | string | Category name. |
| slug | string | URL slug, unique within the store. |
| description | string or null | Category description. |
| image | string or null | Cover image URL. |
| parent_id | uuid or null | The parent category, or null for top level. |
| order | integer | Display position among siblings. |
| created_at | timestamp | When the category was created. |
GET/api/v1/categories
Returns categories oldest first.
Query parameters
| Parameter | Type | Description |
|---|---|---|
| limit | integer | Page size, 1 to 250. Defaults to 50. |
| cursor | string | Opaque cursor from the previous page's meta.next_cursor. |
Endpoint errors: INVALID_PARAM · INVALID_CURSOR
2026-07-08
Store Data API v1 launch
First public release: read-only access to the store snapshot, orders (with line items), customers, products (with variants), and categories. Cursor pagination, incremental sync filters, per-store API keys with instant revocation, and a 120 requests per minute rate limit.
{
"data": {
"id": "7b1e9c40-52aa-4f0e-9d2b-8f6c3a1d5e77",
"name": "Aurora Ceramics",
"store_name": "Aurora Ceramics",
"store_email": "hello@auroraceramics.com",
"store_currency": "USD",
"store_timezone": "America/New_York",
"tax_included_in_prices": false,
"shipping_enabled": true,
"guest_checkout": true,
"customer_accounts": true,
"counts": { "products": 142, "categories": 9, "customers": 830, "orders": 1204 }
}
}{
"data": [ { "id": "…" } ],
"meta": { "next_cursor": "MjAyNi0wNi0xNC…", "has_more": true }
}{
"error": { "code": "INVALID_API_KEY", "message": "Invalid or revoked API key." }
}{
"data": {
"id": "7b1e9c40-52aa-4f0e-9d2b-8f6c3a1d5e77",
"name": "Aurora Ceramics",
"store_name": "Aurora Ceramics",
"store_email": "hello@auroraceramics.com",
"store_currency": "USD",
"store_timezone": "America/New_York",
"tax_included_in_prices": false,
"shipping_enabled": true,
"guest_checkout": true,
"customer_accounts": true,
"counts": { "products": 142, "categories": 9, "customers": 830, "orders": 1204 }
}
}{
"data": {
"id": "9f4e2c1a-7d3b-4e8f-a51c-2b9d6e0f8a34",
"status": "paid",
"total": "93.06",
"items": [ { "name": "Aurora Mug, Glazed Sand", "quantity": 2 } ]
}
}{
"data": [
{
"id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f",
"email": "claire@example.com",
"name": "Claire Fontaine",
"stripe_customer_id": "cus_QxAb4cD5eF6gH7",
"email_verified": "2026-03-02T18:40:12.000Z",
"created_at": "2026-03-02T18:38:55.000Z"
}
],
"meta": { "next_cursor": null, "has_more": false }
}{
"data": {
"id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f",
"email": "claire@example.com",
"name": "Claire Fontaine",
"stripe_customer_id": "cus_QxAb4cD5eF6gH7",
"email_verified": "2026-03-02T18:40:12.000Z",
"created_at": "2026-03-02T18:38:55.000Z"
}
}{
"data": {
"id": "3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
"name": "Aurora Mug",
"slug": "aurora-mug",
"price": "42.00",
"status": "active",
"variants": [ { "sku": "MUG-SAND", "inventory": 18 } ]
}
}{
"data": [
{
"id": "8d9e0f1a-2b3c-4d5e-8f6a-7b8c9d0e1f2a",
"name": "Drinkware",
"slug": "drinkware",
"description": "Mugs, cups, and carafes.",
"image": null,
"parent_id": null,
"order": 1,
"created_at": "2026-02-11T10:00:00.000Z"
}
],
"meta": { "next_cursor": null, "has_more": false }
}{
"data": [
{
"id": "9f4e2c1a-7d3b-4e8f-a51c-2b9d6e0f8a34",
"customer_id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f",
"email": "claire@example.com",
"status": "paid",
"subtotal": "84.00",
"tax": "7.56",
"shipping": "9.90",
"total": "93.06",
"refund_amount": "0.00",
"return_reason": null,
"discount_code": "WELCOME10",
"discount_amount": "8.40",
"shipping_address": {
"firstName": "Claire",
"lastName": "Fontaine",
"line1": "428 Willow Court",
"city": "Portland",
"state": "OR",
"postalCode": "97205",
"country": "US",
"phone": "+1 503 555 0114"
},
"billing_address": null,
"stripe_payment_intent_id": "pi_3RfKw2Ab4cD5eF6g",
"stripe_charge_id": "ch_3RfKw2Ab4cD5eF6g",
"receipt_url": "https://pay.stripe.com/receipts/…",
"tracking_number": null,
"tracking_url": null,
"carrier": null,
"shipping_method": "Standard (3 to 5 days)",
"created_at": "2026-06-14T09:21:33.000Z",
"updated_at": "2026-06-14T09:21:35.000Z",
"items": [
{
"id": "e7a8b9c0-d1e2-4f3a-8b4c-5d6e7f8a9b0c",
"order_id": "9f4e2c1a-7d3b-4e8f-a51c-2b9d6e0f8a34",
"product_id": "3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
"variant_id": null,
"name": "Aurora Mug, Glazed Sand",
"price": "42.00",
"quantity": 2,
"attributes": null,
"created_at": "2026-06-14T09:21:33.000Z"
}
]
}
],
"meta": { "next_cursor": "MjAyNi0wNi0xNCAwOToyMTozM3w5ZjRlMmMxYQ", "has_more": true }
}{
"data": [
{
"id": "3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
"name": "Aurora Mug",
"slug": "aurora-mug",
"description": "A hand-thrown stoneware mug with a glazed interior.",
"price": "42.00",
"compare_price": null,
"images": ["https://media.rovela.app/stores/…/aurora-mug-1.webp"],
"videos": [],
"status": "active",
"category_id": "8d9e0f1a-2b3c-4d5e-8f6a-7b8c9d0e1f2a",
"has_variants": true,
"sku": null,
"inventory": 0,
"track_inventory": true,
"continue_selling_when_out_of_stock": false,
"product_type": "Drinkware",
"vendor": "Aurora Ceramics",
"tags": ["mug", "stoneware"],
"meta_title": null,
"meta_description": null,
"meta_keywords": [],
"metafields": [],
"created_at": "2026-02-11T10:05:00.000Z",
"updated_at": "2026-06-01T15:12:44.000Z",
"variants": [
{
"id": "5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b",
"product_id": "3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
"sku": "MUG-SAND",
"name": "Glazed Sand",
"price": null,
"inventory": 18,
"image": null,
"video": null,
"attributes": { "color": "Sand" },
"created_at": "2026-02-11T10:05:01.000Z"
}
]
}
],
"meta": { "next_cursor": "MjAyNi0wMi0xMSAxMDowNTowMHwzYzRkNWU2Zg", "has_more": true }
}https://rovela.ai/api/v1curl -H "x-api-key: rvk_YOUR_KEY" \
https://rovela.ai/api/v1/storecurl -H "x-api-key: rvk_YOUR_KEY" \
"https://rovela.ai/api/v1/orders?limit=100"curl -H "x-api-key: rvk_YOUR_KEY" \
https://rovela.ai/api/v1/orderscurl -H "x-api-key: rvk_YOUR_KEY" \
https://rovela.ai/api/v1/storecurl -H "x-api-key: rvk_YOUR_KEY" \
https://rovela.ai/api/v1/orders/9f4e2c1a-7d3b-4e8f-a51c-2b9d6e0f8a34curl -H "x-api-key: rvk_YOUR_KEY" \
"https://rovela.ai/api/v1/customers?limit=100"curl -H "x-api-key: rvk_YOUR_KEY" \
https://rovela.ai/api/v1/customers/c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5fcurl -H "x-api-key: rvk_YOUR_KEY" \
https://rovela.ai/api/v1/products/3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6fcurl -H "x-api-key: rvk_YOUR_KEY" \
"https://rovela.ai/api/v1/categories?limit=100"curl -H "x-api-key: rvk_YOUR_KEY" \
"https://rovela.ai/api/v1/orders?status=paid&limit=100"curl -H "x-api-key: rvk_YOUR_KEY" \
"https://rovela.ai/api/v1/products?status=active&limit=100"const API_KEY = process.env.ROVELA_API_KEY;
async function exportAllOrders() {
const orders = [];
let cursor = null;
do {
const url = new URL('https://rovela.ai/api/v1/orders');
url.searchParams.set('limit', '250');
if (cursor) url.searchParams.set('cursor', cursor);
const res = await fetch(url, { headers: { 'x-api-key': API_KEY } });
if (res.status === 429) {
const wait = Number(res.headers.get('Retry-After') ?? 5);
await new Promise((r) => setTimeout(r, wait * 1000));
continue;
}
if (!res.ok) throw new Error(`Export failed: ${res.status}`);
const { data, meta } = await res.json();
orders.push(...data);
cursor = meta.has_more ? meta.next_cursor : null;
} while (cursor);
return orders;
}const url = new URL('https://rovela.ai/api/v1/orders');
url.searchParams.set('limit', '250');
url.searchParams.set('updated_since', lastSyncedAt); // ISO 8601
const res = await fetch(url, { headers: { 'x-api-key': API_KEY } });
const { data } = await res.json();
for (const order of data) upsertByOrderId(order);