E-commerce
E-commerce Customer Authentication
Endpoints for customer registration, login, and password management. All requests should include a valid appID that maps to the tenant site unless your integration is pre-configured on the backend.
POST/customers/signup
Registers a new customer in aurora_customers unless authTable overrides the target table.
Request Body
{
"email": "customer@example.com",
"password": "Str0ngPass!",
"firstname": "Alex",
"lastname": "Taylor",
"displayname": "Alex T",
"appID": 42,
"authTable": "aurora_customers"
}
Success Response (200 OK)
{
"status": "Success",
"_id": "2b9a1c8d1e...", // encrypted internal ID
"email": "customer@example.com",
"emailverified": true,
"firstname": "Alex",
"lastname": "Taylor",
"displayname": "Alex T",
"photourl": "",
"description": ""
}
Validation & business rules: passwords must meet uppercase, lowercase, digit, and special-character requirements (minimum 8 chars). A 409 is returned if the email already exists.
POST/customers/login
Authenticates a customer and returns profile data. Supports optional extra fields via a comma-delimited extraFields string appended to the select list.
Request Body
{
"email": "customer@example.com",
"password": "Str0ngPass!",
"appID": 42,
"authTable": "aurora_customers",
"extraFields": "phone,address"
}
Success Response (200 OK)
{
"status": "Success",
"message": "Login successful",
"data": {
"_id": "7c4f0a1d0b...",
"email": "customer@example.com",
"firstname": "Alex",
"lastname": "Taylor",
"displayname": "Alex T",
"description": "",
"photourl": "",
"emailverified": true,
"phone": "+15551234567",
"address": "123 Main St"
}
}
Error Response (401 Unauthorized)
{
"status": "Failed",
"message": "Invalid credentials",
"data": {}
}
Notes: legacy password hashes are transparently upgraded to bcrypt on successful login. No JWT token is issued by this route.
POST/customers/reset-password
Sets a new password using a previously issued reset token.
Request Body
{
"password": "NewStr0ngPass!",
"confirmPassword": "NewStr0ngPass!",
"token": "abc123def456",
"reset_password_token": "abc123def456",
"authTable": "aurora_customers"
}
Success Response (200 OK)
{
"status": "Success",
"message": "Password reset successfully"
}
Notes: include both token and reset_password_token fields with matching values. The validator expects reset_password_token while the controller reads token. Password strength rules from signup apply.
POST/customers/request-reset
Sends a password reset email with a one-time token.
Request Body
{
"email": "customer@example.com",
"appID": 42,
"url": "https://app.example.com/customer/reset"
}
Success Response (200 OK)
{
"status": "Success",
"message": "Email with reset password instructions was sent to customer@example.com."
}
Errors: if the email is not found, the service throws “No user found” and the request fails. Ensure url points to the frontend route that will capture the token appended by the API.
App User Authentication
Mirrors the customer flows but targets aurora_app_users. Provide authTable if your deployment uses custom tables.
POST/user/app/signup
Registers a new app user. Request and response bodies match /customers/signup; default table is aurora_customers unless authTable is set to aurora_app_users.
POST/user/app/login
Authenticates an app user and returns the same payload structure as customer login. Supply authTable": "aurora_app_users" to query the correct table.
POST/user/app/reset-password
Resets an app user password. Use the same request shape as customer reset with the token fields duplicated and set authTable": "aurora_app_users".
POST/user/app/request-reset
Initiates an app user password reset email. Body and responses mirror /customers/request-reset; the generated link uses the supplied url and token.
E-commerce
The e-commerce API provides comprehensive shopping cart and payment processing capabilities with Stripe integration.
POST/ecommerce/createOrder
Creates a new order in the system.
Request Body
{
"customerID": 123,
"items": [
{
"productID": 101,
"quantity": 2,
"price": 299.99
},
{
"productID": 102,
"quantity": 1,
"price": 399.99
}
],
"shippingAddress": {
"street": "123 Main St",
"city": "New York",
"state": "NY",
"zip": "10001",
"country": "USA"
},
"billingAddress": {
"street": "123 Main St",
"city": "New York",
"state": "NY",
"zip": "10001",
"country": "USA"
}
}
Success Response (201 Created)
{
"status": "Success",
"data": {
"orderID": 5001,
"orderNumber": "ORD-2024-5001",
"total": 999.97,
"status": "pending"
}
}
Cart Management
POST/ecommerce/addCart
Adds an item to the shopping cart.
{
"sessionID": "cart_session_123",
"productID": 101,
"quantity": 1,
"price": 299.99
}
POST/ecommerce/removeCart
Removes an item from the shopping cart.
{
"sessionID": "cart_session_123",
"productID": 101
}
POST/ecommerce/getOrder
Retrieves order details.
{
"orderID": 5001
}
Payment Processing
POST/ecommerce/createPaymentIntent
Creates a Stripe payment intent for processing payment.
Request Body
{
"amount": 99997,
"currency": "usd",
"orderID": 5001,
"customerID": 123
}
Success Response (200 OK)
{
"status": "Success",
"data": {
"clientSecret": "pi_xxxxxxxxxxxxx_secret_xxxxxxxxxxxxx",
"paymentIntentID": "pi_xxxxxxxxxxxxx"
}
}
POST/ecommerce/paymentComplete
Confirms payment completion and updates order status.
Request Body
{
"paymentIntentID": "pi_xxxxxxxxxxxxx",
"orderID": 5001
}
POST/ecommerce/submitOrder
Finalizes and submits the order for processing.
Request Body
{
"orderID": 5001,
"paymentMethod": "stripe",
"notes": "Please deliver after 5 PM"
}
GET/ecommerce/genCode/:id
Generates a QR code for an order or product.
URL Parameters
-
id
number
required
Order ID or product ID for QR code generation.
Success Response
Returns a QR code image in PNG format.
Recommended Checkout Flow
The recommended way to implement a complete purchase uses the single-call checkout pattern. Billing details and Stripe PaymentIntent creation are combined into a single server call; payment confirmation is handled server-side via webhook — no second client API call is needed after the customer pays.
High-Level Steps
- Authenticate — sign up or log in the customer via
POST /customers/login. Store the returned encrypted customer ID. - Create an order — call
POST /ecommerce/createOrder. The API reserves a new order row and returns an encrypted order ID. - Add items to cart — call
POST /ecommerce/addCartfor each product. Items are inserted intoaurora_order_itemswithItemStatus = "Pending". - Single-call checkout — POST the billing details, encrypted IDs, and total amount (in cents) to
POST /ecommerce/checkout. The API saves billing fields to the order record and creates a Stripe PaymentIntent server-side, returning a short-livedclientSecret. - Client-side card collection — pass the
clientSecretto Stripe.jsstripe.confirmCardPayment(). Stripe.js collects the card number, expiry, and CVV inside a Stripe-hosted iframe and sends them directly to Stripe's servers — they never reach this API. - Webhook completes the order (server-side) — once Stripe confirms the charge, it POSTs a
payment_intent.succeededevent toPOST /ecommerce/stripe-webhook/:appID. The API verifies the Stripe signature, then updates the order status to Paid, marks all pending line items as Ordered, sends an admin notification email, and sends the customer a confirmation email — all without further client interaction. - Poll for completion — after step 5 returns without an error, the client polls
GET /ecommerce/orderStatus/:iduntilorderStatusis"Paid". - Display confirmation — once polling confirms payment, redirect to or render the confirmation page.
Step-by-Step Code Walkthrough
Step 1 & 2 — Customer login and create order
// 1. Log in the customer
const loginRes = await fetch('/customers/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'alex@example.com', password: 'Str0ngPass!', appID: 42 })
});
const { data: customer } = await loginRes.json();
const encryptedCustomerID = customer._id; // encrypted ID from the API
// 2. Create an order record
const orderRes = await fetch('/ecommerce/createOrder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ appID: 42, _customerID: encryptedCustomerID })
});
const { data: order } = await orderRes.json();
const encryptedOrderID = order._id; // encrypted order ID — used in all subsequent calls
Step 3 — Add items to cart
await fetch('/ecommerce/addCart', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
appID: 42,
_orderID: encryptedOrderID,
_customerID: encryptedCustomerID,
items: [
{ productID: 101, Qty: 1, Price: 4999 } // price in cents: $49.99
]
})
});
Step 4 — Single-call checkout (billing details + PaymentIntent)
const checkoutRes = await fetch('/ecommerce/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
_orderID: encryptedOrderID,
_customerID: encryptedCustomerID,
appID: 42,
amount: 4999, // total in cents — $49.99 AUD
currency: 'aud',
production: false, // set to true to use the live Stripe key
data: {
firstName: 'Alex',
lastName: 'Taylor',
email: 'alex@example.com',
address: '123 Main St',
suburb: 'Sydney',
postCode: '2000'
}
})
});
const { data: { clientSecret } } = await checkoutRes.json();
// clientSecret is a short-lived token — pass it to Stripe.js immediately
Step 5 — Stripe.js confirms payment (client-side only)
// Load Stripe.js using your app's publishable key (safe to expose client-side).
// Find your publishable key in the Stripe Dashboard under Developers → API keys.
// It starts with 'pk_test_' (test mode) or 'pk_live_' (live mode).
const stripe = Stripe('pk_test_...');
// stripe.confirmCardPayment sends card details DIRECTLY to Stripe — not through this API
const { error } = await stripe.confirmCardPayment(clientSecret, {
payment_method: {
card: cardElement, // a Stripe.js CardElement mounted in your payment form
billing_details: { name: 'Alex Taylor', email: 'alex@example.com' }
}
});
if (error) {
// Card declined or other Stripe error — show message to customer
showError(error.message);
} else {
// Stripe confirmed the charge — begin polling for webhook completion
await pollOrderStatus(encryptedOrderID);
}
Step 6 — Webhook fires automatically (no client code required)
Stripe calls POST /ecommerce/stripe-webhook/:appID with a payment_intent.succeeded event. The server verifies the Stripe signature, looks up the orderID and customerID stored in the PaymentIntent metadata, and runs successfulPayment. See the Business Logic & Status Reference section for the full details of what happens during this step.
Step 7 & 8 — Poll for order status and display confirmation
async function pollOrderStatus(encryptedOrderID, maxAttempts = 10) {
for (let i = 0; i < maxAttempts; i++) {
await new Promise(r => setTimeout(r, 2000 * (i + 1))); // exponential back-off
const res = await fetch(`/ecommerce/orderStatus/${encryptedOrderID}`);
const { data } = await res.json();
if (data.orderStatus === 'Paid') {
redirectToConfirmation(data); // success — show confirmation page
return;
}
if (data.paymentStatus === 'Failed') {
throw new Error('Payment failed — please try again');
}
}
throw new Error('Timed out waiting for payment confirmation');
}
/ecommerce/createPaymentIntent → client confirm → /ecommerce/paymentComplete pattern still works but requires the client browser to make a second API call after the payment. The new /ecommerce/checkout + webhook pattern is preferred because the order is completed entirely server-side and does not depend on the client successfully calling back.
Single-Call Checkout (Stripe)
The frontend calls /ecommerce/checkout with order details and the amount only — the API uses Stripe's server-to-server library to reserve a payment slot ("PaymentIntent") and returns a short-lived clientSecret token. Stripe.js then collects the card number, expiry, and CVV inside a Stripe-hosted iframe in the browser and transmits them directly to Stripe's servers using that token, so no card credential ever reaches this server.
The endpoint persists billing details against the order, creates a Stripe PaymentIntent, and returns the clientSecret that your frontend passes to Stripe.js to collect card details and confirm the payment. Once Stripe confirms the payment it calls the Stripe Webhook endpoint, which updates the order status and sends the confirmation email — no further client action is required.
POST/ecommerce/checkout
Request Body
{
"_orderID": "<encrypted order ID>",
"_customerID": "<encrypted customer ID>",
"appID": 42,
"amount": 9997,
"currency": "aud",
"production": false,
"data": {
"firstName": "Alex",
"lastName": "Taylor",
"email": "alex@example.com",
"address": "123 Main St",
"suburb": "Sydney",
"postCode": "2000"
}
}
Field Notes
- _orderID / _customerID — encrypted IDs as returned by
/ecommerce/createOrder. - amount — amount in the smallest currency unit (cents). E.g.
9997= $99.97 AUD. - currency — ISO 4217 currency code, defaults to
aud. - production —
trueto use the live Stripe key,false(default) for the test key. - data.* — billing address fields only. No card number, CVV, or expiry is accepted or stored here.
Success Response (201 Created)
{
"status": "Success",
"data": {
"clientSecret": "pi_xxxxxxxxxxxxx_secret_xxxxxxxxxxxxx",
"paymentIntentID": "pi_xxxxxxxxxxxxx"
}
}
Pass clientSecret to Stripe.js stripe.confirmCardPayment() to complete the payment on the client side.
Frontend Integration Example
// 1. Call checkout — send order/billing details only, NO card data
const { data } = await api.post('/ecommerce/checkout', payload);
const { clientSecret } = data.data;
// 2. Stripe.js collects card details in its own secure iframe and sends them
// DIRECTLY to Stripe — they never pass through this API
const stripe = Stripe('pk_test_...');
const { error } = await stripe.confirmCardPayment(clientSecret, {
payment_method: { card: cardElement } // cardElement is a Stripe.js Element
});
// 3. Poll order status while Stripe notifies our webhook and completes the order
if (!error) {
pollOrderStatus(encryptedOrderID);
}
Stripe App Configuration
All Stripe credentials and the webhook signing secret are stored per application in the aurora_sites database table. No Stripe keys are held in environment variables or source code.
Required Columns in aurora_sites
| Column | Description | Example value |
|---|---|---|
stripe_test_private_key |
Stripe secret key for test mode. Used when the checkout request contains "production": false. |
sk_test_4eC39Hq… |
stripe_live_private_key |
Stripe secret key for live (production) mode. Used when the checkout request contains "production": true. |
sk_live_51Abc… |
stripe_webhook_secret |
Webhook signing secret starting with whsec_. Used to verify that webhook events originate from Stripe and have not been tampered with. |
whsec_abc123… |
sk_test_ or sk_live_. Store them only in the database. Never include them in source code, config files committed to version control, or any client-side JavaScript.
Getting Your Stripe API Keys
- Log in to the Stripe Dashboard.
- Make sure you are in Test mode (toggle in the top-left of the dashboard) to retrieve test keys, or Live mode for production keys.
- Click the Developers menu (top-right corner) and select API keys.
- Under Standard keys, click Reveal test key (or the live equivalent) and copy the Secret key value.
- Save the value to the appropriate column in
aurora_sitesfor the matching app row:UPDATE aurora_sites SET stripe_test_private_key = 'sk_test_...', stripe_live_private_key = 'sk_live_...' WHERE id = <your appID>;
Configuring the Webhook in Stripe Dashboard
- Log in to the Stripe Dashboard.
- Navigate to Developers → Webhooks and click + Add endpoint.
- Set the Endpoint URL to your API base URL followed by the webhook path. Replace
<appID>with the numeric Infomaxim application ID (the same value used asappIDin all API requests):https://api.yourdomain.com/ecommerce/stripe-webhook/<appID> - Under Select events to listen to, add only
payment_intent.succeeded. This is the only event consumed by the webhook handler. - Click Add endpoint to save.
- On the webhook detail page, find Signing secret and click Reveal. Copy the value — it starts with
whsec_. - Store the signing secret in the database:
UPDATE aurora_sites SET stripe_webhook_secret = 'whsec_...' WHERE id = <your appID>;
Multi-Tenant Configuration
Each row in aurora_sites represents a separate tenant application. This means:
- Each application registers its own Stripe webhook endpoint URL using its own
:appIDpath segment. - Different applications can use different Stripe accounts or the same Stripe account with different webhook secrets.
- The
:appIDin the webhook URL must match the application whose Stripe keys were used when the PaymentIntent was created. The PaymentIntent stores theappIDin its metadata so the webhook handler can verify this.
Local Development & Testing
Use the Stripe CLI to forward live Stripe test events to your local server:
# Install (macOS with Homebrew)
brew install stripe/stripe-cli/stripe
# Authenticate with your Stripe account
stripe login
# Forward payment_intent.succeeded events to your local API
stripe listen --forward-to http://localhost:3001/ecommerce/stripe-webhook/<appID>
The Stripe CLI prints a temporary webhook signing secret (e.g. whsec_abc123…) when it starts. Update stripe_webhook_secret in the database with this value while testing. This secret changes each time stripe listen restarts — remember to update the database value each time.
To simulate a successful payment event without going through the full checkout flow:
stripe trigger payment_intent.succeeded
To test the end-to-end flow with a real (test) card, use Stripe's test card numbers. For example, 4242 4242 4242 4242 with any future expiry date and any CVC will always succeed.
Stripe Webhook
Stripe calls this endpoint when a payment_intent.succeeded event fires. The handler:
- Verifies the Stripe signature using the
stripe_webhook_secretstored for the app — requests with an invalid or missing signature are rejected with HTTP400. - Reads the
orderID,customerID, andappIDfrom the PaymentIntent metadata stored at checkout time. - Calls
successfulPayment, which updates the order status to Paid, marks all pending line items as Ordered, sends an admin notification email, and sends the customer a confirmation email (see Business Logic & Status Reference for full details). - Always returns HTTP
200 { "received": true }— even if downstream processing fails — so Stripe does not retry the event.
POST/ecommerce/stripe-webhook/:appID
The :appID segment identifies the tenant application. Stripe must be configured to POST to the full URL including this segment (see configuration instructions below).
Required App Configuration Fields
The following fields must be set in aurora_sites for the relevant app:
- stripe_live_private_key — Stripe secret key for production payments.
- stripe_test_private_key — Stripe secret key for test payments.
- stripe_webhook_secret — Webhook signing secret obtained from the Stripe Dashboard (see below).
Success Response (200 OK)
{ "received": true }
The endpoint always returns 200 even if downstream processing fails, so that Stripe does not retry the event. Errors are written to the server log for investigation.
For full instructions on configuring the Stripe Dashboard, obtaining the webhook signing secret, and testing locally with the Stripe CLI, see Stripe App Configuration.
Order Status Polling
After the frontend calls /ecommerce/checkout and confirms the payment with Stripe.js, poll this endpoint to detect when the webhook has completed the order. Use exponential back-off and a reasonable timeout (e.g. 30 s) to avoid excessive requests.
GET/ecommerce/orderStatus/:id
URL Parameters
-
id
string
required
Encrypted order ID as returned in the
_idfield by/ecommerce/createOrder.
Success Response (200 OK)
{
"status": "Success",
"data": {
"orderStatus": "Paid",
"orderStatusLabel": "Paid",
"paymentStatus": "Success"
}
}
Typical orderStatus Values
- New — order created, payment not yet received.
- Paid — payment confirmed by Stripe webhook, order complete.
- Ordered — order placed without a card payment (e.g. invoice or loyalty points).
Polling Example
async function pollOrderStatus(encryptedOrderID, maxAttempts = 10) {
for (let i = 0; i < maxAttempts; i++) {
await new Promise(r => setTimeout(r, 2000 * (i + 1))); // back-off
const res = await fetch(`/ecommerce/orderStatus/${encryptedOrderID}`);
const { data } = await res.json();
if (data.orderStatus === 'Paid' || data.orderStatus === 'Ordered') {
return data; // payment complete
}
if (data.paymentStatus === 'Failed') {
throw new Error('Payment failed');
}
}
throw new Error('Timed out waiting for payment confirmation');
}
Business Logic & Status Reference
This section describes what happens inside the API when a payment succeeds — whether triggered by the Stripe webhook or the legacy /ecommerce/submitOrder endpoint.
What Happens on Payment Success (successfulPayment)
When the Stripe webhook receives a payment_intent.succeeded event, it calls the internal successfulPayment function. The following operations run in sequence:
-
Billing details are persisted — the billing fields collected during
/ecommerce/checkout(first name, last name, email, address, suburb, post code) are already saved toaurora_ordersat the time the checkout endpoint is called.successfulPaymentsaves any remaining order fields and the payment reference. -
Order status is updated — the
aurora_ordersrow is updated atomically:orderstatus→"Paid"for card payments via Stripe, or"Ordered"when submitted without a card via/ecommerce/submitOrderorderstatuslabel→ same value asorderstatuspayment_status→"Success"AuthCode→ the Stripe PaymentIntent ID (e.g.pi_3Abc…); used for customer service lookupsTrxnReference→ the same PaymentIntent ID; used to initiate refunds via Stripeorder_date→ current server timestamp (date/time of confirmed payment)
-
Line items are confirmed — every row in
aurora_order_itemswhereOrder_IDmatches andItemStatus = "Pending"is updated toItemStatus = "Ordered". Items that are already"Cancelled"or have other statuses are unaffected. -
Admin notification email is sent — an email is sent to the store's configured administrator recipients. The subject line uses the format
<domain> - Order#:<orderID> Submitted. Recipients are fetched from the app's email configuration. -
Customer confirmation email is sent — a fully rendered HTML email is generated from the
confirmation.htmltemplate and emailed to theBillingEmailaddress on the order. The admin recipients are CC'd. The email includes:- Order ID and confirmed order date
- Each line item: product title, quantity, unit price, and subtotal
- Tax amounts per item
- Bond/deposit amounts (if applicable)
- Order total
- A QR code link for scanning the order
- The app's domain and logo
-
Access tokens are provisioned for digital products — after the emails are dispatched, the system scans every
aurora_order_itemsrow for the order whereIsDigital = 1. For each digital item found a new access token is issued and linked to the customer (see Token Management for full details). - Token provisioning is non-blocking — a failure in this step is logged but does not roll back the order status or prevent the confirmation emails from being sent.
paymentMethodRef is "REDO" (an internal flag used to re-process an order without sending duplicate notifications).
Order Status Values (aurora_orders.orderstatus)
| Value | Meaning | Set by |
|---|---|---|
New |
Order created; payment not yet received. Cart items may exist but the order has not been paid. | Set automatically on order creation via /ecommerce/createOrder. Also reset to New if all items are removed from the cart. |
Paid |
Payment confirmed by Stripe. The customer has been charged successfully. | Set by the Stripe webhook handler when payment_intent.succeeded fires and paymentMethodRef = "Stripe". |
Ordered |
Order placed without a card payment (e.g. invoiced accounts, loyalty/tech fund redemptions, or internal order submissions). | Set by /ecommerce/submitOrder when paymentMethodRef = "Order Only". |
Payment Status Values (aurora_orders.payment_status)
| Value | Meaning |
|---|---|
| (empty / null) | Payment not yet attempted, or a PaymentIntent has been created but Stripe has not confirmed the charge. |
Success |
Payment completed successfully. Set by successfulPayment when the Stripe webhook fires or when an order is submitted without payment. |
Failed |
Payment attempt failed (e.g. card declined). The client should prompt the customer to retry. The order may still exist and can be retried with a new PaymentIntent. |
Order Item Status Values (aurora_order_items.ItemStatus)
| Value | Meaning |
|---|---|
Pending |
Item is in the cart, awaiting payment confirmation. All items added via /ecommerce/addCart start in this status. |
Ordered |
Payment has been confirmed; the item is part of a confirmed order ready for fulfilment. |
Cancelled |
Item was cancelled after being added. Cancelled items are excluded from order totals and confirmation email calculations. |
Gift Voucher |
Item represents a gift voucher. Excluded from freight calculations. |
Email Sequence Summary
Two emails are triggered each time successfulPayment runs:
| # | Recipient | Content | When sent |
|---|---|---|---|
| 1 | Store administrator (configured recipients for the app) | Plain-text notification containing the order ID and customer ID, subject: <domain> - Order#:<orderID> Submitted |
Immediately after the order status is updated to Paid or Ordered |
| 2 | Customer (BillingEmail on the order); admin recipients are CC'd |
Full HTML confirmation using the confirmation.html template — includes all line items, prices, totals, and a QR code link |
Immediately after the admin notification |
Webhook Idempotency & Retries
The Stripe webhook handler always returns HTTP 200 even when downstream processing (database update, email send) fails. This prevents Stripe from automatically retrying the event — retries would cause duplicate emails and duplicate order updates. If successfulPayment throws an error, it is written to the server log for manual investigation. When investigating, check whether the order was already updated before attempting a manual re-run.
Token Management
Access tokens are used to confirm the purchase of digital products and control when and how many times a customer can access them. A token is issued automatically for every digital order item when an order completes; the token can then be validated and activated each time the customer accesses the product.
successfulPayment whenever aurora_order_items.IsDigital = 1.
Business Logic Overview
How a Token Is Issued
- A customer authenticates against
aurora_customers. - The customer completes an
aurora_order(with or without payment). - When the order is confirmed (Stripe webhook fires, or
/ecommerce/submitOrderis called),successfulPaymentruns. - For each
aurora_order_itemsrow whereIsDigital = 1, the system:- Resolves the product from the order item (
product_ID/product_mid) and its table name. - Looks up the
aurora_token_templatelinked to the product viaaurora_related(name = 'aurora_token_template'). - If no template is found, defaults are applied:
status = 'pending',maximum_activations = 3, no expiry date. - Generates a cryptographically secure 64-character hex token string (
crypto.randomBytes(32)). - Inserts a new row into
aurora_tokenswithstatus = 'pending',activations = 0, and the expiry date calculated fromexpiry_daysin the template (orNULLif the template has no expiry). - Links the token to the customer via
aurora_related(parent_table = 'aurora_customers',name = 'aurora_tokens').
- Resolves the product from the order item (
How a Token Is Validated and Activated
- The customer logs in and requests access to the digital product.
- The application calls
POST /ecommerce/validateTokenwith the customer'stokenValueand theproductId. The API checks:- The token exists and matches the product.
- The token
statusis not'expired'. - The
expiry_datehas not passed (if set). If the date has passed, the token is automatically set to'expired'. - The number of
activationsis belowmaximum_activations.
- If the token is valid, the application calls
POST /ecommerce/activateToken. This incrementsactivations, updateslast_activated, and setsstatusto'active'on the very first use.
Token Status Values
| Status | Meaning |
|---|---|
pending | Token issued but not yet used. Default status for a newly created token. |
active | Token has been activated at least once. The customer has successfully accessed the product. |
expired | Token has been explicitly expired, the expiry_date has passed, or access was manually revoked. |
Database Tables
Two new tables support the token system. Run the DDL from docs/aurora_tokens_schema.sql to create them.
| Table | Purpose |
|---|---|
aurora_token_template |
Stores the default configuration values for tokens issued against a product. The admin creates one template row and links it to the product via aurora_related (name = 'aurora_token_template'). |
aurora_tokens |
One row per issued token. Tracks the token value, owning customer and order, validity status, expiry, and activation count. |
Key Columns — aurora_token_template
| Column | Type | Description |
|---|---|---|
name | NVARCHAR(100) | Human-readable name for the template (e.g. "Annual Subscription"). |
status_default | NVARCHAR(20) | Initial status for new tokens — pending or active. Defaults to pending. |
expiry_days | INT (nullable) | Days from purchase date before the token expires. NULL = never expires. |
maximum_activations | INT | Maximum times the token may be activated. Defaults to 3. |
Key Columns — aurora_tokens
| Column | Type | Description |
|---|---|---|
token_value | NVARCHAR(100) UNIQUE | 64-character hex token string stored by the client application. |
product_id | INT | The product this token grants access to. |
customer_id | INT | The customer who purchased the product. |
order_id | INT | The order through which the product was purchased. |
order_item_id | INT | The specific aurora_order_items row that triggered token issuance. |
status | NVARCHAR(20) | pending / active / expired. |
expiry_date | DATETIME (nullable) | Date/time after which the token is no longer valid. NULL = never expires. |
activations | INT | Running count of how many times the token has been activated. |
maximum_activations | INT | Activation limit copied from the template at issuance time. |
last_activated | DATETIME (nullable) | Timestamp of the most recent activation. |
Linking a Template to a Product
Use POST /addrelated to associate a template with a product. The relation must use name = 'aurora_token_template':
await fetch('/addrelated', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
appID: 42,
check: true, // prevent duplicate links
data: {
parent_table: 'products', // your product table name
parent_id: 101, // product row ID
name: 'aurora_token_template',
linked: 5 // aurora_token_template.id
}
})
});
POST/ecommerce/validateToken
Checks whether a token is currently valid for a given product. Call this before granting access to a digital product. The endpoint performs the following checks in order:
- Token exists and belongs to the specified product.
- Token
statusis not'expired'. - Token
expiry_datehas not passed (auto-expires the token if it has). - Token
activationsis belowmaximum_activations.
Request Body
{
"tokenValue": "a3f7c2d1e8b4...",
"productId": 101
}
Parameters
- tokenValue required — the 64-character hex token string from
aurora_tokens.token_value. - productId required — numeric product ID the token must be valid for.
Success Response (200 OK) — token is valid
{
"status": "Success",
"message": "Token is valid",
"data": {
"id": 12,
"token_value": "a3f7c2d1e8b4...",
"product_id": 101,
"customer_id": 55,
"order_id": 5001,
"status": "pending",
"expiry_date": "2027-01-01T00:00:00.000Z",
"activations": 0,
"maximum_activations": 3,
"created_date": "2026-01-15T10:30:00.000Z",
"last_activated": null
}
}
Failure Response (200 OK) — token is not valid
{
"status": "Failed",
"message": "Token has expired",
"data": { ... } // token row if found; null if not found at all
}
Possible message values: "Token not found", "Token has expired", "Token activation limit exceeded", "Error validating token".
POST/ecommerce/activateToken
Records a product access event. Internally calls validateToken first — if the token is not valid the activation is rejected. On success: activations is incremented; last_activated is updated; status changes from 'pending' to 'active' on first use. Call this endpoint each time a customer is granted access, not just the first time.
Request Body
{
"tokenValue": "a3f7c2d1e8b4...",
"productId": 101
}
Success Response (200 OK)
{
"status": "Success",
"message": "Token activated",
"data": { "id": 12, "status": "active", "activations": 1, ... }
}
Typical Integration Pattern
// When a customer requests access to a digital product:
const valRes = await fetch('/ecommerce/validateToken', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tokenValue: storedToken, productId: 101 })
});
const { status } = await valRes.json();
if (status === 'Success') {
// Grant access and record the event
await fetch('/ecommerce/activateToken', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tokenValue: storedToken, productId: 101 })
});
showProduct();
} else {
showAccessDenied();
}
POST/ecommerce/expireToken
Immediately sets a token's status to 'expired', preventing any further access. Use this to manually revoke access (e.g. after a refund or subscription cancellation).
Request Body
{
"tokenId": 12
}
Parameters
- tokenId required — numeric
aurora_tokens.id.
Success Response (200 OK)
{
"status": "Success",
"message": "Token expired"
}
POST/ecommerce/extendToken
Extends a token's expiry_date by a given number of days. If the token is currently 'expired', its status is also reset to 'active'. The new expiry date is calculated from whichever is later: today or the existing expiry date.
Request Body
{
"tokenId": 12,
"days": 30
}
Parameters
- tokenId required — numeric
aurora_tokens.id. - days required — number of days to add. Must be ≥ 1. If missing, zero, or negative, the API returns a
400error (see below).
Success Response (200 OK)
{
"status": "Success",
"message": "Token extended by 30 day(s)",
"data": { "newExpiry": "2027-03-15 10:30:00" }
}
Validation Error (400 Bad Request)
Returned when tokenId is missing or days is missing, zero, or less than 1.
{ "status": "Failed", "message": "tokenId and days (>= 1) are required" }
GET/ecommerce/getToken/:id
Returns full details of a single token row by its internal numeric ID.
URL Parameters
-
id
number
required
Numeric
aurora_tokens.id.
Success Response (200 OK)
{
"status": "Success",
"message": "",
"data": {
"id": 12,
"token_value": "a3f7c2d1e8b4...",
"product_id": 101,
"customer_id": 55,
"order_id": 5001,
"order_item_id": 88,
"status": "active",
"expiry_date": "2027-01-01T00:00:00.000Z",
"activations": 2,
"maximum_activations": 3,
"created_date": "2026-01-15T10:30:00.000Z",
"last_activated": "2026-02-10T14:22:00.000Z"
}
}
Not Found (404)
{ "status": "Failed", "message": "Token not found", "data": null }
POST/ecommerce/getCustomerTokens
Returns all tokens owned by a customer, optionally filtered to a specific product. Use this to display a customer's purchased digital products or to check whether they already hold a valid token.
Request Body
{
"customerId": 55,
"productId": 101 // optional — omit to return all tokens for the customer
}
Parameters
- customerId required — numeric
aurora_customers.id. - productId optional — when supplied, only tokens for that product are returned.
Success Response (200 OK)
{
"status": "Success",
"message": "",
"data": [
{
"id": 12,
"token_value": "a3f7c2d1e8b4...",
"product_id": 101,
"order_id": 5001,
"status": "active",
"expiry_date": "2027-01-01T00:00:00.000Z",
"activations": 2,
"maximum_activations": 3,
"created_date": "2026-01-15T10:30:00.000Z",
"last_activated": "2026-02-10T14:22:00.000Z"
}
]
}
Returns an empty array ([]) when no tokens match.