E-commerce

Authentication endpoints for customers and app users are now documented on the Customer Management page. This page focuses on checkout, cart, payments, and token commerce flows.

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.

Customer identity: do not submit a numeric customerID. If a valid customer JWT is present, the order is linked to that customer. If no customer is authenticated yet, the API creates a temporary placeholder customer profile and links the order to it; once the customer authenticates later, the order is reconciled to the real profile.

Request Body

{
  "appID": 42,
  "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
}

Security note: customer identity is never taken from body fields. The API derives customer context from JWT when available, otherwise keeps the order's placeholder customer.

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
}

Security note: customerID is not accepted from the client in payment endpoints.

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

  1. Optional authenticate — if the customer is already signed in, include their bearer JWT in requests. If not, guest checkout can start immediately.
  2. Create an order — call POST /ecommerce/createOrder. The API reserves a new order row and returns an encrypted order ID. For guests, a placeholder customer profile is created automatically.
  3. Add items to cart — call POST /ecommerce/addCart for each product. Items are inserted into aurora_order_items with ItemStatus = "Pending".
  4. Single-call checkout — POST the billing details, encrypted order ID, and total amount (in cents) to POST /ecommerce/checkout. If the customer has authenticated by this stage, the API reconciles the order from placeholder customer to the authenticated customer profile before payment completion.
  5. Client-side card collection — pass the clientSecret to Stripe.js stripe.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.
  6. Webhook completes the order (server-side) — once Stripe confirms the charge, it POSTs a payment_intent.succeeded event to POST /ecommerce/stripe-webhook. The API verifies the Stripe signature, resolves app context from verified event metadata, 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.
  7. Poll for completion — after step 5 returns without an error, the client polls GET /ecommerce/orderStatus/:id until orderStatus is "Paid".
  8. Display confirmation — once polling confirms payment, redirect to or render the confirmation page.

Step-by-Step Code Walkthrough

Step 1 & 2 — Optional login and create order

// 1. Optional: log in the customer (if already known)
const loginRes = await fetch('/customers/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email: 'alex@example.com', password: 'Str0ngPass!', appID: 42 })
});
// Keep your JWT access token from auth flow for authenticated requests.

// 2. Create an order record
const orderRes = await fetch('/ecommerce/createOrder', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ appID: 42 })
});
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',
    // Optional but recommended when customer is authenticated
    'Authorization': `Bearer ${accessToken}`
  },
  body: JSON.stringify({
    appID: 42,
    _orderID: encryptedOrderID,
    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',
    // Optional but recommended when customer is authenticated
    'Authorization': `Bearer ${accessToken}`
  },
  body: JSON.stringify({
    _orderID: encryptedOrderID,
    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 with a payment_intent.succeeded event. The server verifies the Stripe signature, resolves app context and order context from PaymentIntent metadata (appID, orderID, customerID), 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') {
      // data.giftTokens (if any) can be rendered on confirmation page
      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');
}
Legacy two-step flow: The older /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)

Card credentials never pass through this API.

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>",
  "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 — encrypted order ID as returned by /ecommerce/createOrder.
  • Authorization: Bearer ... optional during guest carting, recommended at checkout to reconcile the order to an authenticated customer profile.
  • amount — amount in the smallest currency unit (cents). E.g. 9997 = $99.97 AUD.
  • currency — ISO 4217 currency code, defaults to aud.
  • productiontrue to 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

ColumnDescriptionExample 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…

Publishable Key Returned by POST /getApp

The frontend should fetch Stripe publishable keys from POST /getApp. The returned property name depends on server environment:

  • Production (config.app.production = true) — response includes stripe_live_public_key.
  • Stage/Test (config.app.production = false) — response includes stripe_test_public_key.

Request Body

{
  "appID": 42
}

Example Response (Production)

{
  "logo": "...",
  "colour1": "#1f2a44",
  "colour2": "#f4b400",
  "stripe_live_public_key": "pk_live_...",
  "_id": "...",
  "_publicKey": "AABBCC"
}

Example Response (Stage/Test)

{
  "logo": "...",
  "colour1": "#1f2a44",
  "colour2": "#f4b400",
  "stripe_test_public_key": "pk_test_...",
  "_id": "...",
  "_publicKey": "AABBCC"
}
Security: Stripe secret keys begin with 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

  1. Log in to the Stripe Dashboard.
  2. 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.
  3. Click the Developers menu (top-right corner) and select API keys.
  4. Under Standard keys, click Reveal test key (or the live equivalent) and copy the Secret key value.
  5. Save the value to the appropriate column in aurora_sites for 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

  1. Log in to the Stripe Dashboard.
  2. Navigate to Developers → Webhooks and click + Add endpoint.
  3. Set the Endpoint URL to your API base URL followed by the shared webhook path:
    https://api.infomaxim.com/ecommerce/stripe-webhook
  4. Under Select events to listen to, add only payment_intent.succeeded. This is the only event consumed by the webhook handler.
  5. Click Add endpoint to save.
  6. On the webhook detail page, find Signing secret and click Reveal. Copy the value — it starts with whsec_.
  7. 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:

  • All applications share the same Stripe webhook endpoint URL: https://api.infomaxim.com/ecommerce/stripe-webhook.
  • Different applications can use different Stripe accounts or the same Stripe account with different webhook secrets.
  • The PaymentIntent stores appID in metadata, and the webhook handler uses verified metadata to resolve tenant context.

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

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:

  1. Verifies the Stripe signature using the stripe_webhook_secret stored for the app — requests with an invalid or missing signature are rejected with HTTP 400.
  2. Reads the orderID, customerID, and appID from the PaymentIntent metadata stored at checkout time.
  3. 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).
  4. Always returns HTTP 200 { "received": true } — even if downstream processing fails — so Stripe does not retry the event.

POST/ecommerce/stripe-webhook

This is the preferred shared webhook endpoint. App context is resolved from verified Stripe event metadata. Legacy route /ecommerce/stripe-webhook/:appID remains available for backward compatibility.

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 _id field by /ecommerce/createOrder.

Success Response (200 OK)

{
  "status": "Success",
  "data": {
    "orderStatus": "Paid",
    "orderStatusLabel": "Paid",
    "paymentStatus": "Success",
    "giftTokens": [
      {
        "id": 99,
        "token_value": "a3f7c2d1e8b4...",
        "product_id": 101,
        "order_item_id": 88,
        "status": "gift_pending",
        "product_title": "Digital Course"
      }
    ]
  }
}

Gift tokens on confirmation page: when an order contains gift digital items, the polling payload includes data.giftTokens once the order is paid. Render these values in your confirmation page so the purchaser can share them with the recipient.

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:

  1. Billing details are persisted — the billing fields collected during /ecommerce/checkout (first name, last name, email, address, suburb, post code) are already saved to aurora_orders at the time the checkout endpoint is called. successfulPayment saves any remaining order fields and the payment reference.
  2. Order status is updated — the aurora_orders row is updated atomically:
    • orderstatus"Paid" for card payments via Stripe, or "Ordered" when submitted without a card via /ecommerce/submitOrder
    • orderstatuslabel → same value as orderstatus
    • payment_status"Success"
    • AuthCode → the Stripe PaymentIntent ID (e.g. pi_3Abc…); used for customer service lookups
    • TrxnReference → the same PaymentIntent ID; used to initiate refunds via Stripe
    • order_date → current server timestamp (date/time of confirmed payment)
  3. Line items are confirmed — every row in aurora_order_items where Order_ID matches and ItemStatus = "Pending" is updated to ItemStatus = "Ordered". Items that are already "Cancelled" or have other statuses are unaffected.
  4. 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.
  5. Customer confirmation email is sent — a fully rendered HTML email is generated from the confirmation.html template and emailed to the BillingEmail address 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 amount (shown only when a non-zero bond exists, formatted to 2 decimal places)
    • Order total (labelled as Total (inc GST))
    • A QR code link for scanning the order
    • The app's domain and logo
    • A closing message of Thank you for your booking. when the order contains non-cancelled aurora_events items, otherwise Thank you for your order.
  6. Access tokens are provisioned for digital products — the system scans every aurora_order_items row for the order where IsDigital = 1. Standard items create customer-owned tokens; gift-marked items create gift_pending tokens that are included in confirmation output and redeemed later by the recipient (see Token Management for full details).
  7. 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.
Note: No emails are sent if the paymentMethodRef is "REDO" (an internal flag used to re-process an order without sending duplicate notifications).

Order Status Values (aurora_orders.orderstatus)

ValueMeaningSet 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)

ValueMeaning
(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)

ValueMeaning
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:

#RecipientContentWhen 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. Gift purchases are also supported using the same token table and status lifecycle.

Automatic issuance: You do not need to call any endpoint to create tokens. They are provisioned server-side as part of successfulPayment whenever aurora_order_items.IsDigital = 1. If an order item is marked as a gift (RedeemStatus = 'gift' or 'gift_pending'), the token is created with status = 'gift_pending', is not linked to the purchaser, and is included in confirmation output for sharing with the recipient.

Business Logic Overview

How a Token Is Issued

  1. A customer authenticates against aurora_customers.
  2. The customer completes an aurora_order (with or without payment).
  3. When the order is confirmed (Stripe webhook fires, or /ecommerce/submitOrder is called), successfulPayment runs.
  4. For each aurora_order_items row where IsDigital = 1, the system:
    1. Resolves the product from the order item (product_ID / product_mid) and its table name.
    2. Looks up the aurora_token_template linked to the product via aurora_related (name = 'aurora_token_template').
    3. If no template is found, defaults are applied: status = 'pending', maximum_activations = 3, no expiry date.
    4. Generates a cryptographically secure 64-character hex token string (crypto.randomBytes(32)).
    5. Inserts a new row into aurora_tokens with status = 'pending', activations = 0, and the expiry date calculated from expiry_days in the template (or NULL if the template has no expiry).
    6. Links the token to the customer via aurora_related (parent_table = 'aurora_customers', name = 'aurora_tokens').

How a Token Is Validated and Activated

  1. The customer logs in and requests access to the digital product.
  2. The application calls POST /ecommerce/validateToken with either:
    • tokenValue + productId (explicit token validation), or
    • productId only (customer must be authenticated as aurora_customers; API finds a valid token for that customer).
    The API checks:
    • The token exists and maps to the requested product master_id (via aurora_data.current_id -> aurora_data.master_id for source_table = 'aurora_products').
    • The token is not in gift_pending state (gift tokens must be redeemed first with /ecommerce/redeemToken).
    • The token status is not 'expired'.
    • The expiry_date has not passed (if set). If the date has passed, the token is automatically set to 'expired'.
    • The number of activations is below maximum_activations.
  3. If the token is valid, the application calls POST /ecommerce/activateToken. This increments activations, updates last_activated, and sets status to 'active' on the very first use.

Token Status Values

StatusMeaning
pendingToken issued but not yet used. Default status for a newly created token.
gift_pendingGift token issued to an order but not yet claimed by a recipient account.
activeToken has been activated at least once. The customer has successfully accessed the product.
expiredToken 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.

TablePurpose
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

ColumnTypeDescription
nameNVARCHAR(100)Human-readable name for the template (e.g. "Annual Subscription").
status_defaultNVARCHAR(20)Initial status for new tokens — pending or active. Defaults to pending.
expiry_daysINT (nullable)Days from purchase date before the token expires. NULL = never expires.
maximum_activationsINTMaximum times the token may be activated. Defaults to 3.

Key Columns — aurora_tokens

ColumnTypeDescription
token_valueNVARCHAR(100) UNIQUE64-character hex token string stored by the client application.
product_idINTThe product row version ID (aurora_products.id current at purchase time). Validation maps this to the product master_id through aurora_data.
customer_idINTThe customer who purchased the product.
order_idINTThe order through which the product was purchased.
order_item_idINTThe specific aurora_order_items row that triggered token issuance.
statusNVARCHAR(20)pending / active / expired.
expiry_dateDATETIME (nullable)Date/time after which the token is no longer valid. NULL = never expires.
activationsINTRunning count of how many times the token has been activated.
maximum_activationsINTActivation limit copied from the template at issuance time.
last_activatedDATETIME (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
    }
  })
});

Gift Token Purchase Flow

Use this flow when a purchaser is buying a digital product for someone else. The purchaser receives the token in their confirmation, then the recipient redeems the token into their own account.

How to mark a cart line as a gift

When adding the digital item, set redeemStatus to "gift" (or "gift_pending").

await fetch('/ecommerce/addCart', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    appID: 42,
    orderID: encryptedOrderID,
    items: [
      {
        productID: 101,
        Qty: 1,
        redeemStatus: 'gift'
      }
    ]
  })
});

Gift behavior at payment completion

  1. Order is completed via /ecommerce/submitOrder, /ecommerce/paymentComplete, or Stripe webhook.
  2. Token is issued with status = 'gift_pending' in aurora_tokens.
  3. Token is not linked to purchaser via aurora_related and does not appear in purchaser auto-validation.
  4. Gift token values are included in confirmation output (email and payment status payload where applicable) so they can be shared with the recipient.

Recipient redemption flow (example)

// 1) Recipient logs in and gets a customer JWT
// 2) Recipient redeems gifted token
const redeemRes = await fetch('/ecommerce/redeemToken', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${customerAccessToken}`
  },
  body: JSON.stringify({
    tokenValue: giftTokenFromPurchaser,
    productId: 101 // product master_id (optional but recommended)
  })
});

const redeemJson = await redeemRes.json();
if (redeemJson.status === 'Success') {
  // token now belongs to recipient and is already activated
  showProduct();
} else {
  showError(redeemJson.message);
}

Database impact for gift flow

  • No new columns required.
  • Gift intent is read from aurora_order_items.RedeemStatus (existing column).
  • Gift lifecycle is represented in aurora_tokens.status using gift_pending (existing column).
  • Ownership transfer uses existing aurora_tokens.customer_id and aurora_related link rows.

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 supports two modes:

  • Explicit token mode: pass tokenValue + productId.
  • Authenticated customer mode: pass productId only with a valid customer bearer token; the API finds a valid token for that customer/product pair.

The endpoint performs the following checks in order:

  1. Token exists (explicit mode) or at least one token exists for the authenticated customer (customer mode).
  2. Token maps to the specified product master_id (via aurora_data.current_id = aurora_tokens.product_id and source_table = 'aurora_products').
  3. Token status is not 'expired'.
  4. Token expiry_date has not passed (auto-expires the token if it has).
  5. Token activations is below maximum_activations.

Request Body — explicit token mode

{
  "tokenValue": "a3f7c2d1e8b4...",
  "productId": 101
}

Request Body — authenticated customer mode (no tokenValue)

{
  "productId": 101
}

Parameters

  • tokenValue optional — the 64-character hex token string from aurora_tokens.token_value. Omit this only when the customer is authenticated and you want customer-based lookup.
  • productId required — product master_id (not the version row ID).

Authentication note: if tokenValue is omitted, a valid customer bearer token is required. Otherwise the API returns 401.

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", "No valid token found for customer and product", "Token has expired", "Token activation limit exceeded", "Token product version not found", "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();
}

Typical Integration Pattern (no stored token)

// Customer is already authenticated (JWT bearer token available)
const valRes = await fetch('/ecommerce/validateToken', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${accessToken}`
  },
  body: JSON.stringify({ productId: 101 })
});
const { status, data } = await valRes.json();

if (status === 'Success') {
  // data contains the matched valid token row
  showProduct();
} else {
  showAccessDenied();
}

POST/ecommerce/redeemToken

Redeems a gifted token into the currently authenticated customer. This endpoint is used by recipients of gift purchases. On success the token is transferred to the recipient and activated in one operation.

Request Body

{
  "tokenValue": "a3f7c2d1e8b4...",
  "productId": 101
}

Parameters

  • tokenValue required — gifted token value from purchaser confirmation.
  • productId optional — product master_id. Recommended to prevent redeeming a token for an unexpected product.

Authentication required: include a valid customer bearer token. The recipient customer is always resolved from JWT; no body customerId is accepted.

Success Response (200 OK)

{
  "status": "Success",
  "message": "Gift token redeemed",
  "data": {
    "id": 99,
    "token_value": "a3f7c2d1e8b4...",
    "customer_id": 55,
    "status": "active",
    "activations": 1,
    "master_product_id": 101
  }
}

Failure Response (400 Bad Request)

{
  "status": "Failed",
  "message": "Token is not redeemable as a gift",
  "data": null
}

Typical failure messages include: Token not found, Token has expired, Token activation limit exceeded, Token product version not found, and Token is not redeemable as a gift.

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 400 error (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 the currently authenticated 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

{
  "productId": 101   // optional — omit to return all tokens for the customer
}

Authentication required: include a valid customer bearer token. The API derives customerId from JWT and ignores any submitted numeric customer ID.

Parameters

  • 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.