REST API Reference
Endpoints, authentication, pagination, rate limits, and errors for the E2C Store REST API.
The E2C Store API gives you access to your own store's data over HTTPS, returned as JSON. Use it to pull new orders into a spreadsheet, sync stock and prices with another system, add products from your inventory tool, or trigger a Zapier workflow when a sale comes in.
Most endpoints only read. Products and categories can also be created and edited by keys with the matching Add & edit permission. Nothing in the API deletes.
Base URL
https://e2c.store/api/v1/All paths below are relative to this base URL.
Quick start
- In your dashboard, go to Settings → API keys and create a key. See API keys.
- Copy the key — it's shown once.
- Call
GET /store, which works with any key:
export E2C_API_KEY="e2c_sk_your_key_here"
curl "https://e2c.store/api/v1/store" \
-H "Authorization: Bearer $E2C_API_KEY"{
"data": {
"name": "Maple & Thread",
"currency": "CAD",
"timezone": "America/Toronto",
"api_key": { "scopes": ["orders:read", "products:read"] }
}
}In Zapier, use a Webhooks by Zapier GET step or an API Request action with the same URL and Authorization header.
Authentication
Every request needs an Authorization: Bearer <key> header. Keys start with e2c_sk_.
A key sent in the URL (as api_key, apikey, key, token, or access_token) is rejected with 401 invalid_api_key, even if the key is correct, because URLs end up in logs. Treat a key like a password. If one leaks, revoke it in Settings → API keys; revoking takes effect immediately.
A key stops working when it is revoked, when it expires, or if the store is no longer active.
Permissions (scopes)
Each key carries one or more permissions. Calling an endpoint without the permission it needs returns 403 insufficient_scope.
| Scope | Shown in settings as | Grants |
|---|---|---|
orders:read | Orders | Orders, items, shipping, and tracking, including the buyer's name and shipping address. |
products:read | Products | Physical and digital products, variants, stock, and their reviews. |
services:read | Services | Services and their reviews. |
events:read | Events | Events, ticket types and sales, and their reviews. |
customers:read | Customers | The customer list. Also adds the buyer's email and phone to order responses. |
reports:read | Reports | Sales, revenue trend, order status, top products, and traffic reports. |
categories:write | Add & edit categories | Create categories, and rename, recode, show, or hide them. |
products:write | Add & edit products | Create physical products, and edit the details, price, stock, and visibility of physical and digital products. |
Reading categories and reviews needs any one of products:read, services:read, or events:read, and only shows the listing types the key can read. Write permissions don't include read access.
Endpoints
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /store | Any key | Store name, currency, timezone, and this key's scopes |
| GET | /orders | orders:read | List orders |
| GET | /orders/{order_number} | orders:read | One order with items and tracking |
| GET | /products | products:read | List physical and digital products |
| GET | /products/{code} | products:read | One product with description, shipping, variants, and combinations |
| POST | /products | products:write | Create a physical product |
| PATCH | /products/{code} | products:write | Edit a physical or digital product |
| GET | /services | services:read | List services |
| GET | /services/{code} | services:read | One service with description |
| GET | /events | events:read | List events |
| GET | /events/{code} | events:read | One event with description and ticket types |
| GET | /categories | Any catalog read | All categories (never paginated) |
| GET | /categories/{code} | Any catalog read | One category |
| POST | /categories | categories:write | Create a category |
| PATCH | /categories/{code} | categories:write | Rename, recode, show, or hide a category |
| GET | /reviews | Any catalog read | Published reviews |
| GET | /customers | customers:read | One entry per customer email |
| GET | /reports/sales | reports:read | Sales summary |
| GET | /reports/revenue-trend | reports:read | Daily revenue |
| GET | /reports/order-status | reports:read | Order count per status |
| GET | /reports/top-products | reports:read | Best sellers by revenue |
| GET | /reports/traffic | reports:read | Sessions, visitors, page views, and funnel |
Orders
GET /orders returns orders newest first. It includes every checkout status — abandoned and failed checkouts too, not just completed sales. List entries don't include items or tracking; retrieve the order for those.
| Parameter | Type | Description |
|---|---|---|
status | string | Only orders with this status code, e.g. CONF, SHIP, DEL. |
created_since | date | ISO 8601 date or date-time. Inclusive. |
created_before | date | ISO 8601 date or date-time. Exclusive. |
limit | integer | 1–100, default 25. |
cursor | string | The next_cursor from the previous page. |
GET /orders/{order_number} uses the order number shown in your dashboard.
{
"data": {
"order_number": 1042,
"created_at": "2026-09-01T14:03:00Z",
"status": "SHIP",
"status_label": "Shipped",
"currency": "CAD",
"subtotal": 39.98,
"shipping_price": 5.5,
"total": 45.48,
"paid": true,
"payment_method": "stripe",
"shipping_method": "Standard",
"item_types": ["product"],
"note": "",
"customer": { "name": "Ada Lovelace", "email": "[email protected]", "phone": "555-0100" },
"shipping_address": { "line1": "1 Main St", "line2": "", "city": "Toronto", "state": "ON", "postal_code": "M5V 1A1", "country": "Canada" },
"items": [
{ "type": "product", "product_code": "linen-tee", "product_name": "Linen Tee", "variant": "Size: M", "quantity": 2, "unit_price": 19.99, "shipping_per_item": 2.75 }
],
"tracking": { "carrier": "UPS", "tracking_number": "1Z999", "tracking_details": "", "ready_for_pickup": false, "updated_at": "2026-09-02T09:00:00Z" }
}
}customer.emailandcustomer.phoneonly appear when the key also hascustomers:read.item_typesand each item'stypeareproduct,digital,service, orevent. Event tickets are always ordered on their own.- An item's
product_codeisnullif the listing has since been deleted.variantisnullfor products without variants.trackingisnulluntil you add shipping details. payment_methodisstripe,paypal,cash,interac, orzelle.
Order status codes
| Code | Label | Code | Label |
|---|---|---|---|
ABORT | Aborted | RFP | Ready For Pickup |
PP | Pending Payment | CAP | Cash at Pickup |
PROC-PAY | Processing Payment | DEL | Delivered |
PAY-FAIL | Payment Failed | REFUND | Refunded |
CONF | Confirmed | RETURN | Returned |
PROC-ORD | Processing Order | CANCEL | Cancelled |
SHIP | Shipped | CLOSE | Closed |
COMP | Complete | SCHEDULED | Scheduled |
ABORT, PP, PROC-PAY, and PAY-FAIL are checkouts that never became a sale.
Building a "new order" automation? Poll GET /orders without a status filter and skip those four checkout states. Filtering on CONF alone misses cash orders (CAP), pay-after-service bookings (SCHEDULED), and orders that moved on before your next poll.
Products, services, and events
GET /products, GET /services, and GET /events share the same parameters and return newest-created first. Deleted listings are never returned. Events are sorted by when they were created, not by event date.
| Parameter | Type | Description |
|---|---|---|
status | string | active, inactive, or incomplete. |
updated_since | date | ISO 8601. Covers creation and edits made in the dashboard editor. Stock changes from sales and quick status toggles don't count as updates. |
category | string | Only listings in the category with this code. |
q | string | Search by name or code (up to 100 characters). |
limit | integer | 1–100, default 25. |
cursor | string | The next_cursor from the previous page. |
A product looks like this (list entries leave out description, keywords, shipping, variants, and combinations):
{
"data": {
"code": "linen-tee",
"name": "Linen Tee",
"sku": "LT-001",
"brand": "Maple & Thread",
"category": { "code": "tops", "name": "Tops" },
"status": "active",
"currency": "CAD",
"price": 19.99,
"compare_at_price": 24.99,
"short_description": "Breathable everyday tee.",
"created_at": "2026-08-12T16:20:00Z",
"updated_at": "2026-09-03T10:45:00Z",
"stock": 42,
"is_digital": false,
"gtin": "",
"mpn": "",
"condition": "new",
"condition_other": "",
"local_pickup": false,
"keywords": ["linen", "summer"],
"description": "Cut from 100% European linen...",
"shipping": [
{ "method": "Standard Shipping", "delivery_time": "7-14 business days", "price": 5.5 }
],
"variants": [
{ "name": "Size", "options": ["S", "M", "L"] }
],
"combinations": [
{ "options": ["M"], "price": 21.99, "stock": 8 }
]
}
}codeidentifies a listing everywhere in the API. You can change a product's code in the dashboard, so update any stored codes if you rename one.compare_at_priceis the struck-through "was" price, ornull.conditionisnew,refurbished,used_like_new,used_very_good,used_good,used_acceptable, orother—nullfor digital products.descriptionis the HTML from your product editor. A combination'spriceorstockisnullwhen not set for that combination.
Services add service_type (hourly, daily, project, recurring, or in_person), price_label, location (type is remote, in_person, or both), delivery_estimate, and pay_after_service.
Events add starts_at, ends_at (both UTC; ends_at may be null), timezone, location (type is venue, online, or tba), min_age, age_policy, event_policy, and contact. GET /events/{code} also returns ticket_types, each with name, description, price, capacity, sold, and available. capacity and available are null for unlimited tickets; sold includes tickets held in checkouts still in progress.
Categories
GET /categories returns all categories sorted by name. A store has at most 25 categories, so has_more is always false.
{
"data": [
{ "code": "tops", "name": "Tops", "status": "active", "counts": { "products": 14, "services": 0, "events": 0 } }
],
"has_more": false,
"next_cursor": null
}counts only includes the listing types your key can read.
Reviews
GET /reviews returns published reviews, newest first.
| Parameter | Type | Description |
|---|---|---|
product_code | string | Only reviews for this product, service, or event. |
created_since | date | ISO 8601 date. Compared by day. |
limit, cursor | As above. |
Each review has product_code, product_name, reviewer_name, rating (average of the three ratings), ratings (quality, price, value, each 1–5), summary, content, and posted_on (a date without time).
Customers
GET /customers returns one entry per customer email, newest customers first, with email, name, phone, order_count, first_order_at, and last_order_at. Name and phone come from the customer's most recent order. Takes limit and cursor.
Reports
These return the same numbers you see on your dashboard.
| Path | Parameters | Returns |
|---|---|---|
/reports/sales | days 1–365, default 7 | period_days, total_orders, pending_orders, total_sales, total_refunds, total_revenue |
/reports/revenue-trend | days 1–90, default 30 | currency and a days array of date, revenue, order_count (days with no orders included as zeros) |
/reports/order-status | — | Array of status, status_label, count, across all time |
/reports/top-products | days 1–365, default 30; limit 1–50, default 10 | Array of product_code (null if deleted), name, units_sold, revenue |
/reports/traffic | days 1–90, default 30 | period_days, sessions, unique_visitors, page_views, and a funnel with product_views, cart_visits, checkout_starts, checkout_completes, product_view_to_order_rate (a percentage) |
Writing data
Create with POST and edit with PATCH, sending a JSON object with Content-Type: application/json (up to 64 KB). A PATCH only changes the fields you send. Send null (or "") to clear an optional field. Unknown fields are rejected, so a typo never fails silently.
A successful POST returns 201, a successful PATCH returns 200, both with the saved record in data, exactly as a GET returns it. Writes count toward the same rate limit and appear in your store's audit history under the API key's name.
The API never deletes. To hide a product or category, set its status to inactive; delete it in the dashboard.
Product fields
| Field | Type | Rules |
|---|---|---|
name | string | Up to 50 characters. Unique across your products, services, and events. Required on create. |
price | number | In your store's currency, up to two decimals. Required on create. |
code | string | Up to 50 lowercase letters, numbers, and single hyphens. Made from the name if left out. |
compare_at_price | number | The "was" price. null or 0 removes it. |
stock | integer | 0–1,000,000. |
status | string | active or inactive. New products are active unless you send inactive. |
category | string | A category code. null removes the product from its category. |
sku, brand, gtin, mpn | string | Optional identifiers. |
condition | string | One of the condition values above. Physical products only. |
condition_other | string | Required when condition is other. |
short_description | string | Up to 150 characters of plain text. |
description | string | Up to 20,000 characters. Plain text or simple HTML (paragraphs, line breaks, bold, italic, underline, strikethrough, headings, lists, blockquotes, and https/mailto links). Other tags are removed but their text is kept. |
keywords | string[] | Replaces the existing list; [] clears it. |
shipping | object[] | Up to 3 options, each with method, delivery_time, and price (0 for free). One must be Standard Shipping, and each method can appear once. Replaces the existing list. Physical products only. |
local_pickup | boolean | Whether buyers can pick up locally. Physical products only. |
Shipping method is one of: Standard Shipping, Economy Shipping, Express Shipping, Expedited Shipping, Standard Air Shipping, Standard Sea Shipping. delivery_time is one of: 1-2 business days, 1-7 business days, 7-14 business days, 14-21 business days, 21-28 business days, 1-2 months.
curl -X POST "https://e2c.store/api/v1/products" \
-H "Authorization: Bearer $E2C_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Linen Tee", "price": 19.99, "stock": 42, "category": "tops", "shipping": [{ "method": "Standard Shipping", "delivery_time": "7-14 business days", "price": 5.5 }] }'Things to know when writing products:
POST /productsonly creates physical products. Images, variants, and digital files are added in the dashboard, so digital products, services, and events can't be created through the API.- A new product is live straight away, so it needs at least one shipping option or
"local_pickup": true— unless you send"status": "inactive". PATCHreturns409 conflictif it would leave an active physical product with no shipping and no local pickup, or if you setpriceorstockon a product whose variants have their own prices or stock (change those in the dashboard).- To change a product's code, send the new one as
code; the old code stops working right away. PATCHon a service or event code returns404.condition,condition_other,shipping, andlocal_pickupcan't be set on digital products.- A store holds at most 100 listings; past that,
POSTreturns409 limit_reached.
Category fields
| Field | Type | Rules |
|---|---|---|
name | string | Up to 25 characters. Unique in your store, and can't be "All". Required on create. |
code | string | Up to 25 lowercase letters, numbers, and single hyphens. Made from the name if left out. |
status | string | active shows it on your storefront; inactive hides it. |
Products in a category stay in it when its code changes. A store has at most 25 categories; past that, POST returns 409 limit_reached.
Pagination
List endpoints return up to limit items (default 25, max 100), newest first:
{
"data": [ { "order_number": 1042 } ],
"has_more": true,
"next_cursor": "ml-k3Q58S106wVawzKYPjKh88VSVcUhMVINaOHqpYMiBAA"
}When has_more is true, pass next_cursor as the cursor parameter to get the next page. On the last page, next_cursor is null. Cursors are opaque: pass them back exactly as received and don't store them long-term. New items never shift existing pages, so polling the first page reliably catches new orders.
Rate limits
Each key can make 60 requests per minute. Responses that pass authentication and the permission check include:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed per minute |
X-RateLimit-Remaining | Requests left in the current window |
X-RateLimit-Reset | When the window resets, as Unix time |
Over the limit you get 429 rate_limited with a Retry-After header in seconds.
Dates and money
- Times are ISO 8601 in UTC, like
2026-09-26T14:03:00Z. Date filters accept a date (2026-09-01, meaning midnight UTC) or a full date-time with an offset. - Order and product times recorded before June 20, 2026 were stored in US Eastern time and may read up to 5 hours early.
- Money values are numbers with up to two decimals, with a currency code on the same object.
Errors
Errors use standard HTTP status codes and a JSON body:
{ "error": { "code": "insufficient_scope", "message": "This API key needs the 'products:read' permission." } }| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_parameter | A query parameter or body field is missing or out of range. The message names it. |
| 400 | invalid_body | The request body is not a JSON object. |
| 400 | prohibited_content | The text contains content that isn't allowed on E2C stores. |
| 401 | invalid_api_key | The key is missing, wrong, revoked, or expired — or was sent in the URL. |
| 403 | insufficient_scope | The key lacks the permission this endpoint needs. |
| 404 | not_found | The endpoint, order, product, or category doesn't exist in your store. |
| 405 | method_not_allowed | The endpoint doesn't support this method. The Allow header lists the ones it does. |
| 409 | conflict | The change clashes with your store as it is now, such as a name or code already in use. |
| 409 | limit_reached | Your store already has the most categories or listings it can hold. |
| 413 | payload_too_large | The request body is larger than 64 KB. |
| 415 | unsupported_media_type | A write was sent without Content-Type: application/json. |
| 429 | rate_limited | Too many requests. Wait for the seconds in Retry-After. |
| 500 | internal_error | Something went wrong on our side. Retry later. |