The ScaleEdges API follows REST principles: resource-based URLs, standard HTTP methods and status codes, and JSON responses. Request bodies may be sent as JSON or form-encoded data.
Use API v2 to read your store's products, customers and orders, manage coupons, receive automation events, and verify or update product license keys. Download the OpenAPI 3.1 specification for machine-readable request and response schemas.
Base URL
https://www.scaleedges.com/api/v2
Use the API from a trusted server. Never put an account API key or product secret in browser JavaScript, a mobile application, a public repository, an error message, or analytics.
Account authentication
Open Settings → API as the store owner. Enter a name such as Zapier or Make, select permissions, then click Create API key. Its complete value is displayed once. Store it in your server-side secret manager. You may keep up to 25 active keys; creating a key does not replace existing keys.
Send the key as a Bearer token:
curl https://www.scaleedges.com/api/v2/coupons \
--header "Authorization: Bearer YOUR_API_KEY"
You may instead send the scaleedges-api-key header. The payhip-api-key header is also accepted to simplify migration from a Payhip-compatible integration.
Use the individual Rotate API key or Revoke API key button beside the credential. Rotation invalidates only that key's previous token, preserves its permissions and transfers its webhook subscriptions to the replacement. Update that integration with the new token. Revocation disables that key's subscriptions and cancels pending deliveries; a request already in flight may still reach the receiver. Other keys remain active. ScaleEdges stores only one-way hashes of account keys.
Read permissions are preselected when creating a key in Settings. Enable write permissions only when the integration needs them. Existing keys retain access to all account API resources after the migration.
| Permission | Access | | --- | --- | | account:read | Test the connection and identify the store | | products:read | Products, variants and collections | | customers:read | Customer records and customer events | | orders:read | Orders and order/subscription events | | events:read | Event history; requires the relevant resource read permission too | | coupons:read | List, search and retrieve coupons | | coupons:write | Create, update and delete coupons | | webhooks:write | Manage the current key's subscriptions, delivery history and replays |
Missing permissions return 403 insufficient_scope. A product license secret does not replace an account API key.
Connection test
GET /account requires account:read. Use it to validate credentials in Zapier, Make or another connector. Do not use the public API index (GET /api/v2) as an authentication test.
{
"data": {
"id": "42",
"name": "My Store",
"store_slug": "my-store",
"currency": "EUR",
"scopes": ["account:read", "products:read"]
},
"request_id": "request-example"
}
Responses and errors
Successful responses put the resource in data. Paginated responses also include meta:
{
"data": { "coupons": [] },
"meta": {
"limit": 10,
"offset": 0,
"total": 0,
"has_more": false
},
"request_id": "44c40597-4522-4245-ab89-2d1bbd3e1125"
}
Errors have a stable machine-readable code and a human-readable message:
{
"error": {
"type": "invalid_request_error",
"code": "invalid_discount",
"message": "Provide exactly one of percent_off or amount_off."
},
"request_id": "44c40597-4522-4245-ab89-2d1bbd3e1125"
}
Common status codes are 200 for success, 201 for creation, 204 for deletion, 400 for invalid input, 401 for invalid credentials, 404 for a resource outside the authenticated scope, 409 for a conflict, and 429 for a rate limit.
Every response includes X-Request-Id. Include it when contacting support. Account endpoints share a limit of 120 requests per minute per store across all its keys, plus 240 per minute per source IP. License endpoints have separate limits of 180 per minute per product and 360 per minute per IP. Rate-limited responses include Retry-After; wait that many seconds before retrying. Do not automatically retry validation or authentication failures.
Products, collections, customers and orders
All routes are relative to /api/v2. IDs in these new resources are strings; keep them as returned. All monetary values use the smallest currency unit, and dates use RFC 3339 with UTC timestamps.
| Method and path | Required permission | Optional list filters | | --- | --- | --- | | GET /products | products:read | product_key (exact slug) | | GET /products/:id | products:read | — | | GET /collections | products:read | — | | GET /customers | customers:read | email (exact, case-insensitive) | | GET /customers/:id | customers:read | — | | GET /orders | orders:read | customer_id, product_id | | GET /orders/:id | orders:read | — |
Products expose id, product_key, name, price_cents, currency, type, published, created_at and updated_at. A product detail also includes variants with ID, name, price and currency. Both published and draft products in your store are available; deleted products and private buyer content are omitted. Collections expose id, name, slug and created_at for use in coupon selectors.
Customers expose id, email, name and created_at; erased customers are omitted. This is the store's customer list, not its newsletter followers. Creating a customer record does not imply a completed payment or marketing consent.
Orders contain a stable ord_… ID, customer ID/email/name, amount_cents, confirmed refunded_cents, currency, payment_provider, kind, optional subscription_id, created_at and an items array. Each item includes the purchase ID (nullable for a subscription payment), product ID/name/slug, variant ID/name, quantity, line total in amount_cents, and coupon code. Buyer identity fields become null after account erasure.
A paid cart is one order with multiple items. Subscription payments, including renewals, produce separate orders. A bundle is represented by the purchased bundle, without additional sales for its included access grants. Free purchases are also orders. API order IDs differ from the dashboard's ORD-… display numbers. New orders preserve price and product snapshots, including separate records for repurchases. Existing recorded purchases and paid subscription transactions are backfilled without sending historical events; payment history already merged by the legacy repurchase system cannot be reconstructed. Imported CSV sales remain in the sales-import interface and are not replayed as new order events.
Cursor pagination
New list endpoints (products, collections, customers, orders, events, webhook-subscriptions and webhook-deliveries) accept limit from 1 to 100 (default 25). They return records in descending insertion order. Pass meta.next_cursor back as cursor, preserving the endpoint and filters, until has_more is false. New insertions do not move records between pages already being read.
{
"data": { "customers": [] },
"meta": { "limit": 25, "has_more": false, "next_cursor": null },
"request_id": "request-example"
}
Coupons retain their existing limit/offset pagination for compatibility.
Idempotent creation
Send an Idempotency-Key header with POST /coupons, POST /webhook-subscriptions or POST /webhook-deliveries/:id/retry. Use 8–200 letters, digits, dots, colons, underscores or hyphens, such as a UUID. Keys are scoped to the API credential, method and endpoint and retained for 24 hours.
An identical retry returns the original successful status and data, with Idempotency-Replayed: true. A different body with the same key returns 409 idempotency_conflict. Failed requests are not cached. Mutation and successful response storage commit together. After 24 hours, or using a different API credential, the request is treated as new. Product license usage counters do not support this mechanism.
Events
GET /events lists recent events; GET /events/:id retrieves one. Both require events:read plus the resource permission. The list silently omits event types outside your permissions; explicitly requesting an inaccessible type returns 403.
Optional filters are type and since (an inclusive RFC 3339 timestamp). Events are retained for 30 days, with queued deliveries retained until processing reaches a terminal state. Only events produced after the automation API is installed are available. Removing a customer's account also removes their event snapshots and pending deliveries.
| Event | Payload under data | Additional permission | | --- | --- | --- | | order.created | order snapshot after a confirmed payment or free checkout; includes subscription payments | orders:read | | order.refunded | order plus refund.amount_cents (new confirmed amount) and refund.total_refunded_cents | orders:read | | customer.created | customer with ID, email, name and creation time | customers:read | | subscription.updated | subscription with status, previous status, product/version, price, currency, renewal period, trial end and cancellation settings | orders:read |
Order and subscription events require orders:read; customer events require customers:read. Pending or failed refunds do not emit order.refunded. Replayed confirmations with unchanged totals do not generate another event. A subscription event is emitted when tracked state actually changes, including local expiration and cancellation changes; repeated identical provider updates are ignored. A new customer inserted during an import can emit customer.created, even though imported sales do not emit order.created.
{
"id": "evt_example",
"type": "customer.created",
"created_at": "2026-09-05T14:32:10.000Z",
"data": {
"customer": {
"id": "123",
"email": "buyer@example.com",
"name": "Example Buyer",
"created_at": "2026-09-05T14:32:10.000Z"
}
}
}
This is the webhook body. API responses wrap it in the usual data envelope. Use the event's id to deduplicate polling results and webhook deliveries.
Durable webhooks
Create a webhook in Settings → API → Webhooks or use the subscription endpoints. A key needs webhooks:write, events:read and the resource read permissions for its selected events. Up to 100 subscriptions are allowed per store. The URL must use public HTTPS on port 443. Credentials and fragments in the URL, private IP addresses, private DNS answers and redirects are rejected. Each connection uses the DNS address checked for that delivery.
Subscribe and unsubscribe
POST /webhook-subscriptions
{
"url": "https://your-automation.example/webhook",
"events": ["order.created", "order.refunded"]
}
The 201 response contains data.id, url, events, status, created_at and a secret beginning with whsec_. Save the secret securely: it is only shown at creation, or in the original idempotent response during the 24-hour retry window. Creating the same URL and event selection again with that key returns the existing subscription without showing its secret again.
GET /webhook-subscriptions lists only the current key's subscriptions. DELETE /webhook-subscriptions/:id returns 204, removes that subscription and its delivery history, and cancels its pending deliveries. Deleting an already absent subscription also returns 204. Other API keys cannot list, remove or replay this connection's webhooks. The store owner can manage all subscriptions in Settings.
The subscription starts with future events; historical events are not automatically delivered. For a Zapier REST Hook, submit Zapier's target URL when the Zap activates and delete the returned subscription ID when it deactivates. For Make, use the same API in the webhook's attach/detach operations. No callback challenge is required. These endpoints also support Integrately's webhook/API workflows; they do not by themselves publish a branded app in any platform's directory.
Verify a delivery
Requests include ScaleEdges-Event-Id, ScaleEdges-Delivery-Id and:
ScaleEdges-Signature: t=UNIX_SECONDS,v1=HEX_SIGNATURE
The signature is HMAC-SHA256 with your subscription's signing secret over timestamp + "." + rawBody. Verify the original bytes before parsing JSON and reject timestamps more than five minutes from your clock. Each new attempt receives a fresh timestamp and signature; its event ID remains unchanged.
const crypto = require('node:crypto');
function verifyWebhook(rawBody, header, secret) {
const match = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(header || '');
if (!match || Math.abs(Date.now() / 1000 - Number(match[1])) > 300) return false;
const expected = crypto.createHmac('sha256', secret)
.update(match[1] + '.').update(rawBody).digest();
return crypto.timingSafeEqual(expected, Buffer.from(match[2], 'hex'));
}
Return a 2xx response after durably accepting the event, then process it asynchronously. Delivery is at least once: a receiver may accept a request before the sender loses its connection or restarts. Make your receiver idempotent using event.id. Delivery ordering is not guaranteed; use the event snapshot for that event and GET /orders/:id for current order totals.
Retries, delivery logs and replays
Events and destination deliveries are saved with the originating database transaction. Network sends happen only after commit. A background worker checks the queue every five seconds; its 60-second delivery leases allow another process to recover work after an interrupted worker.
Network failures, HTTP 408, 429 and 5xx are retried up to 10 attempts in a delivery cycle. Delays between attempts are 1 minute, 5 minutes, 15 minutes, 1 hour, 3 hours, 6 hours, 12 hours, 24 hours and 24 hours. A longer Retry-After is respected up to 24 hours. Delivery requests time out after 10 seconds, in addition to the bounded DNS check. Other 4xx, redirects and rejected destinations stop that delivery. 410 Gone disables the subscription and cancels its remaining queued deliveries.
GET /webhook-deliveries returns delivery IDs, event/subscription IDs, state, attempt count, next attempt, last HTTP status/error, delivery date, and the 20 latest attempts. Filter by subscription_id; use cursor pagination for older deliveries. Response bodies from receivers and signing secrets are not logged. States are pending, sending, delivered, failed and canceled.
POST /webhook-deliveries/:id/retry returns 202 and requeues a failed or delivered event on an active subscription. It preserves the event and delivery IDs and their history, and resets the attempt counter. The receiver may run again if it has not retained a deduplication record. The owner can also use Replay in Settings. A disabled subscription must be removed and recreated before receiving future events.
Operating the worker
The worker starts automatically with npm start. A separate worker or scheduler can run npm run webhooks; npm run maintenance also processes pending deliveries and retention. All workers must share the same persistent SQLite data volume. On a deployment with no long-lived application process, schedule the standalone command regularly.
Webhook signing secrets and idempotent responses are encrypted with api_webhook_encryption.key beside app.db. Preserve that file when moving or restoring a database. Automatic SQLite backups copy it to a matching .db.api-webhook.key sidecar when present; restore that sidecar as api_webhook_encryption.key next to the restored database. Do not generate a replacement key for an existing database: old secrets would become unreadable.
Coupons
A coupon applies either a percentage discount or a fixed discount in the store currency. Fixed monetary values use the smallest currency unit: for example, 500 is €5.00 when the store currency is EUR.
Coupon object
id— integer identifier.coupon_type—singlefor one product,multifor the entire store, orcollection.percent_off— number greater than 0 and at most 100, ornull.amount_off— positive integer in the smallest currency unit, ornull.code— checkout code.start_dateandend_date— RFC 3339 datetimes ornull.product_key— product URL slug whencoupon_typeissingle.collection_id— collection identifier whencoupon_typeiscollection.minimum_purchase_amount— minimum subtotal in the smallest currency unit, ornull.usage_limit— maximum redemptions, ornull.times_redeemed— current redemption count.notes— private seller note, ornull.created_at— RFC 3339 creation datetime.
Example:
{
"id": 1495522137,
"coupon_type": "single",
"percent_off": 25,
"amount_off": null,
"code": "WELCOME25",
"start_date": "2026-08-10T08:00:00.000Z",
"end_date": null,
"product_key": "example-product",
"collection_id": null,
"minimum_purchase_amount": null,
"usage_limit": 100,
"times_redeemed": 0,
"notes": "Launch campaign",
"created_at": "2026-08-10T07:30:00.000Z"
}
Create a coupon
POST /coupons
Provide exactly one of percent_off or amount_off. product_key is required for single; collection_id is required for collection.
curl https://www.scaleedges.com/api/v2/coupons \
--request POST \
--header "Authorization: Bearer YOUR_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"code": "WELCOME25",
"coupon_type": "single",
"product_key": "example-product",
"percent_off": 25,
"usage_limit": 100
}'
Optional fields are start_date, end_date, minimum_purchase_amount, usage_limit, and notes. Dates must include a timezone, such as 2026-08-31T23:59:59Z. Coupon codes contain up to 40 letters, numbers, underscores, or hyphens and are saved in uppercase.
List coupons
GET /coupons
Use limit from 1 to 100 (default 10) and a non-negative offset (default 0). Add code for an exact case-insensitive coupon-code search.
curl --get https://www.scaleedges.com/api/v2/coupons \
--data-urlencode "limit=10" \
--data-urlencode "offset=0" \
--header "Authorization: Bearer YOUR_API_KEY"
Referral and loyalty rewards are omitted because they are controlled by their respective programs.
Retrieve, update, or delete a coupon
GET /coupons/:idretrieves one coupon.PATCH /coupons/:idorPUT /coupons/:idupdates the supplied fields.DELETE /coupons/:iddeletes the coupon and returns204 No Content.
To switch discount kind during an update, set the old field to null and provide the new one:
curl https://www.scaleedges.com/api/v2/coupons/1495522137 \
--request PATCH \
--header "Authorization: Bearer YOUR_API_KEY" \
--header "Content-Type: application/json" \
--data '{"percent_off": null, "amount_off": 500}'
Automatic license keys
ScaleEdges can generate one unique license key per buyer, product, version, and license type. Multiple License key blocks with the same type reuse the same key; different types create separate keys.
Configure a product
- Open a digital product and select its Content tab.
- In a buyer content page, open the block menu and add License key. This block is intentionally unavailable in the public product description.
- Enter a type such as Standard, Pro, or Team, the buyer-facing label, optional activation instructions, and an optional maximum number of activations. Zero means unlimited.
- If the product has versions, configure each version's pages independently. A buyer receives only the license types present in the purchased version.
- Save the product. ScaleEdges creates a product API secret and displays its complete value once in Advanced options → Content.
The key appears automatically in purchased content with a copy button, enabled state, and optional usage count. Keys are generated through the shared order service for free orders, paid Stripe orders, and included bundle products. Existing purchases receive their missing keys the next time purchased content is opened.
Rotating the product secret invalidates the previous secret without changing customer license keys. A completed full refund disables all keys attached to the refunded payment; a failed refund restores keys that were disabled by that refund.
Product authentication
License endpoints use the product-specific secret, not the account API key:
product-secret-key: YOUR_PRODUCT_SECRET
The secret works only for license keys belonging to that product. You may also send it as a Bearer token.
License key object
license_key— unique customer key.enabled— whether the seller has enabled the key.valid—trueonly when the key is enabled and has not reached its activation limit.product_link— product URL slug.buyer_email— email used for the purchase.uses— recorded activation count.max_uses— activation limit, ornullfor unlimited.license_type— configured license type.product_name— product title.variant_nameandvariant_id— purchased version, ornull.date— RFC 3339 key creation datetime.
{
"enabled": true,
"valid": true,
"product_link": "example-product",
"license_key": "7NQ2M-VK5AR-3YD8P-XC6TW",
"buyer_email": "buyer@example.com",
"uses": 1,
"max_uses": 3,
"license_type": "Pro",
"product_name": "Example Product",
"variant_name": "Professional",
"variant_id": "42",
"date": "2026-08-10T08:30:00.000Z"
}
Verify a license key
GET /license/verify
Verification does not change the usage counter.
curl --get https://www.scaleedges.com/api/v2/license/verify \
--data-urlencode "license_key=7NQ2M-VK5AR-3YD8P-XC6TW" \
--header "product-secret-key: YOUR_PRODUCT_SECRET"
Inspect valid, not only enabled: an enabled key whose maximum activation count has been reached is no longer valid for a new activation.
Enable or disable a license key
PUT /license/disabledisables a key.PUT /license/enablere-enables a key.
curl https://www.scaleedges.com/api/v2/license/disable \
--request PUT \
--header "product-secret-key: YOUR_PRODUCT_SECRET" \
--data-urlencode "license_key=7NQ2M-VK5AR-3YD8P-XC6TW"
Increase or decrease usage
PUT /license/usageadds one use. It returns409 usage_limit_reachedwhen the configured maximum has already been reached and409 license_disabledfor a disabled key.PUT /license/decreasesubtracts one use without going below zero.
curl https://www.scaleedges.com/api/v2/license/usage \
--request PUT \
--header "product-secret-key: YOUR_PRODUCT_SECRET" \
--data-urlencode "license_key=7NQ2M-VK5AR-3YD8P-XC6TW"
Usage mutations are not idempotent: send each activation or deactivation event once. Verify the returned uses value before retrying after an uncertain network outcome.
Security checklist
- Keep account keys and product secrets server-side and redact them from logs.
- Use one product secret only with its matching product.
- Rotate a credential immediately if it may have been exposed.
- Check
validbefore granting access, then increment usage only after a successful activation. - Decrease usage when a device is deactivated.
- Treat
401as a credential problem and404as an unknown resource within the authenticated scope; ScaleEdges does not reveal resources from another store or product.