API Routes Reference
Overview
UberLotto uses React Router v7 file-based routing with the ($locale). prefix convention for i18n support. Routes are organized as:
- API routes (
api.*) — Backend endpoints for payments, data, and integrations - Page routes (
($locale).*) — User-facing pages with server-side loaders - SEO routes — Sitemaps, robots.txt, and other crawler-facing responses
Route File Naming Convention
| Pattern | Example | URL |
|---|---|---|
api.name.ts | api.plisio-invoice.ts | /api/plisio-invoice |
api.name.$param.ts | api.get-product.$handle.ts | /api/get-product/:handle |
($locale).name.tsx | ($locale)._index.tsx | / or /:locale |
[name].tsx | [robots.txt].tsx | /robots.txt |
Payment API Endpoints
POST /api/plisio-invoice
Creates a cryptocurrency payment invoice via the Plisio gateway.
File: app/routes/api.plisio-invoice.ts
| Property | Details |
|---|---|
| Methods | POST (action), OPTIONS (loader) |
| Authentication | Required — Shopify Customer Account login (customerAccount.isLoggedIn()) |
| CORS | Origin-restricted via getAllowedOrigin() (same-origin + localhost in dev) |
| Rate Limiting | Max 5 pending transactions per email; max 3 transactions per minute per email |
Request Body:
{
"amount": 50,
"email": "user@example.com",
"customer_name": "John",
"customer_lastname": "Doe",
"customer_id": "gid://shopify/Customer/123"
}Response (200):
{
"success": true,
"data": {
"txn_id": "abc123",
"invoice_url": "https://plisio.net/invoice/abc123",
"amount": "0.025",
"wallet_hash": "0x...",
"currency": "ETH",
"source_currency": "USD",
"source_rate": "2000.00",
"qr_code": "data:image/png;base64,...",
"order_number": "UL-1234567890",
"expected_confirmations": 3
}
}Security Measures:
- Customer must be logged in (
context.customerAccount.isLoggedIn()) - Amount validation with precision exploit prevention (NaN, Infinity, scientific notation)
- Email format validation (RFC 5321 limit: 254 chars)
- Failed validation logged to Supabase security events
- Pending transaction saved to
payment_transactionstable before invoice creation; updated with Plisio'sprovider_txn_idandprovider_invoice_urlafter invoice is created - CORS restricted to same-origin and localhost in development via shared
getAllowedOrigin() - No-cache response headers
GET/POST /api/plisio-webhook
Handles Plisio payment notification callbacks with a 6-layer defense-in-depth security model.
File: app/routes/api.plisio-webhook.ts
| Property | Details |
|---|---|
| Methods | GET (loader), POST (action) |
| Authentication | HMAC signature verification |
| Rate Limiting | IP-based, per-transaction, and global rate limits |
Security Layers:
| Layer | Protection | Response on Failure |
|---|---|---|
| 1 | IP Whitelisting (if PLISIO_WEBHOOK_IPS configured) | 403 Forbidden |
| 2 | Rate Limiting (IP + global) | 429 / 503 |
| 3 | HMAC Signature Validation | 401 Unauthorized |
| 4 | Replay Attack Prevention (nonce-based) | 409 Conflict |
| 5 | Transaction Rate Limiting | 429 Too Many Requests |
| 6 | Duplicate Transaction Check (database) | 200 Already Processed |
Processing: After passing all security layers, the webhook payload is processed and the transaction is saved to Supabase. Amount and currency validation occur during processing.
WARNING
Both GET and POST handlers implement the full 6-layer security stack. GET support exists for backward compatibility with older webhook integrations.
GET /api/plisio-status
Polls the current status of a Plisio cryptocurrency transaction.
File: app/routes/api.plisio-status.ts
| Property | Details |
|---|---|
| Methods | GET (loader), OPTIONS (action) |
| Authentication | Required — Shopify Customer Account login (customerAccount.isLoggedIn()) |
| CORS | Origin-restricted via getAllowedOrigin() (same-origin + localhost in dev) |
| Rate Limiting | None |
Query Parameters:
| Parameter | Required | Description |
|---|---|---|
txn_id | Yes | Plisio transaction ID |
Response (200):
{
"success": true,
"data": {
"txn_id": "abc123",
"status": "confirmed",
"plisio_status": "completed",
"amount": "0.025",
"currency": "ETH",
"order_number": "UL-1234567890",
"tx_url": "https://etherscan.io/tx/0x..."
}
}Status Mapping: Plisio statuses are mapped to internal PaymentStatus values (completed, pending, error). The payment_transactions record in Supabase is looked up by provider_txn_id and updated on each status check.
POST /api/moonpay-sign
Signs MoonPay widget URLs using HMAC-SHA256 for secure fiat-to-crypto payment integration.
File: app/routes/api.moonpay-sign.ts
| Property | Details |
|---|---|
| Methods | POST (action), OPTIONS (loader) |
| Authentication | Required — Shopify Customer Account login |
| Rate Limiting | IP-based (via Supabase) |
Request Body:
{
"url": "https://buy.moonpay.com/?apiKey=...¤cyCode=ETH"
}Response (200):
{
"success": true,
"signature": "base64-encoded-hmac-sha256"
}Security Measures:
- Customer must be logged in (
context.customerAccount.isLoggedIn()) - URL host validation — only allows
buy.moonpay.com,buy-sandbox.moonpay.com,sell.moonpay.com,sell-sandbox.moonpay.com - CORS restricted to same-origin and localhost in development via shared
getAllowedOrigin() - HMAC-SHA256 signing via Web Crypto API (Cloudflare Workers compatible)
- IP-based rate limiting (via Supabase, when configured)
Transaction Pre-Creation:
Before signing the URL, the endpoint pre-creates a pending payment_transactions record in Supabase using parameters extracted from the MoonPay widget URL (externalTransactionId, externalCustomerId, baseCurrencyAmount, baseCurrencyCode, walletAddress). This allows the MoonPay webhook to match incoming notifications to existing transactions via order_number (= externalTransactionId). If pre-creation fails, the URL is still signed (non-blocking).
POST /api/moonpay-webhook
Handles MoonPay transaction notification webhooks with a 5-layer security pipeline and full transaction lifecycle management.
File: app/routes/api.moonpay-webhook.ts
| Property | Details |
|---|---|
| Methods | POST (action) |
| Authentication | HMAC-SHA256 signature via Moonpay-Signature-V2 header |
| Rate Limiting | IP-based (100/min) and global (1000/min) |
Security Layers:
| Layer | Protection | Response on Failure |
|---|---|---|
| 1 | IP rate limiting (100 req/min per IP) | 429 Too Many Requests |
| 2 | Global rate limiting (1000 req/min) | 503 Service Unavailable |
| 3 | HMAC-SHA256 signature verification (timestamp freshness + constant-time comparison) | 401 Unauthorized |
| 4 | Replay attack prevention (nonce-based via checkGenericWebhookNonce()) | 200 Duplicate acknowledged |
| 5 | Payload structure validation + security event logging | 200 (logged) |
HMAC-SHA256 Verification Details:
- Header format:
Moonpay-Signature-V2: t=TIMESTAMP,s=SIGNATURE - Signed payload:
TIMESTAMP.BODY(timestamp + literal period + raw body) - Signature encoding: hex (NOT base64)
- Timestamp freshness: rejects webhooks older than 5 minutes
- Comparison: XOR-based constant-time function
Replay Protection:
- Nonce string:
SHA-256(moonpay_txn_id:status:event_type) - Uses
checkGenericWebhookNonce()from sharedreplay-protection.server.ts - Stores nonces in shared
webhook_noncestable with 5-minute TTL - Fail-open on database errors
Transaction Processing:
After passing security layers, the webhook processes transactions into the payment_transactions table using a three-step lookup strategy:
- Look up by
provider_txn_id(MoonPay's internal ID) — matches previously updated records - Look up by
order_number(=externalTransactionId) — matches pre-created pending records from the sign endpoint - If neither found — create a new transaction record
Status Mapping:
| MoonPay Status | Internal PaymentStatus |
|---|---|
completed | completed |
failed | failed |
waitingPayment, waitingAuthorization | processing |
pending | pending |
Payload Validation:
- Handles MoonPay sending
dataas a JSON string (double-parse) - Validates required fields:
type,data.id
Webhook Event Types: transaction_created, transaction_updated, transaction_failed
TIP
Returns 200 OK even on internal errors to prevent MoonPay from retrying, per MoonPay's best practices documentation.
POST /api/shopify-checkout
Creates a Shopify checkout cart for credit loading (UBL Points purchases).
File: app/routes/api.shopify-checkout.ts
| Property | Details |
|---|---|
| Methods | POST (action) |
| Authentication | Required — Shopify Customer Account (handleAuthStatus()) |
| Rate Limiting | Relies on Shopify's built-in API rate limits |
Request Body:
{
"amount": "50",
"email": "user@example.com",
"customerAccessToken": "token"
}Response (200):
{
"success": true,
"checkoutUrl": "https://checkout.shopify.com/...",
"cart": {
"id": "gid://shopify/Cart/123",
"totalQuantity": 1,
"totalAmount": "50.00",
"currencyCode": "USD"
}
}Security Measures:
- Content-Type validation (must be
application/json) - Request body size limit (1KB max)
- CSRF protection via origin/referer validation
- Customer authentication required
- Comprehensive request body validation
- Price manipulation prevention (variant price vs. requested amount check with 1 cent tolerance)
Error Codes: METHOD_NOT_ALLOWED, INVALID_CONTENT_TYPE, PAYLOAD_TOO_LARGE, MISSING_ORIGIN, INVALID_ORIGIN, AUTHENTICATION_REQUIRED, INVALID_JSON, VALIDATION_FAILED, AMOUNT_UNAVAILABLE, PRICE_MISMATCH, STOREFRONT_UNAVAILABLE, CHECKOUT_FAILED
Data API Endpoints
GET/POST /api/wallet-transactions
CRUD operations for payment transaction records in the payment_transactions table.
File: app/routes/api.wallet-transactions.ts
| Property | Details |
|---|---|
| Methods | GET (loader), POST (action), OPTIONS |
| Authentication | Required — Shopify Customer Account login (customerAccount.isLoggedIn()) |
| CORS | Origin-restricted via getAllowedOrigin() (same-origin + localhost in dev) |
| Rate Limiting | In-memory: 30 requests per minute per IP |
GET — Fetch Transaction History
Fetches transaction history for the authenticated user from payment_transactions.
Query Parameters:
| Parameter | Required | Description |
|---|---|---|
email | One of email/wallet | User email address |
wallet_address | One of email/wallet | Ethereum wallet address (0x...) |
limit | No | Max results (default: 50) |
Response:
{
"transactions": [
{
"id": "uuid",
"user_email": "john@example.com",
"order_number": "UL-1234567890",
"provider": "plisio",
"provider_txn_id": "abc123",
"amount": 50.00,
"amount_usd": 50.00,
"currency": "USD",
"crypto_currency": "ETH",
"wallet_address": "0x...",
"tx_hash": "0x...",
"status": "completed",
"created_at": "2025-01-15T10:30:00Z",
"completed_at": "2025-01-15T10:35:00Z"
}
]
}POST — Create Transaction
Creates a new record in the payment_transactions table. This endpoint is primarily for server-side/internal use — transactions are normally created by the invoice and webhook flows.
Request Body Fields:
| Field | Required | Type | Validation |
|---|---|---|---|
user_email | Yes | string | Email format |
order_number | Yes | string | Non-empty |
provider | Yes | string | Must be moonpay or plisio |
amount | Yes | number | Positive number |
user_name | No | string | Customer first name |
user_lastname | No | string | Customer last name |
user_id | No | string | Customer ID |
provider_txn_id | No | string | Provider's transaction ID |
provider_invoice_url | No | string | Provider's invoice URL |
amount_usd | No | number | Positive number |
currency | No | string | Defaults to USD |
crypto_currency | No | string | Crypto currency code |
wallet_address | No | string | Ethereum address format (0x + 40 hex chars) |
tx_hash | No | string | Transaction hash |
conversion_rate | No | number | Positive number |
status | No | string | One of: pending, processing, awaiting_confirmation, completed, failed, cancelled, expired, refunded, partially_refunded, error |
metadata | No | object | Arbitrary JSON metadata |
expires_at | No | string | ISO 8601 expiration timestamp |
Security Headers: All responses include X-Content-Type-Options: nosniff, X-Frame-Options: DENY, X-XSS-Protection: 1; mode=block.
GET /api/get-product/:handle
Fetches full product details with lottery drawing data.
File: app/routes/api.get-product.$handle.ts
| Property | Details |
|---|---|
| Methods | GET (loader) |
| Authentication | None |
| Rate Limiting | None |
URL Parameters:
| Parameter | Description |
|---|---|
handle | Shopify product handle (e.g., powerball-ticket) |
Response: Returns product data from Shopify Storefront API including variants, options, selected variant, and enriched drawing data fetched from Supabase jackpots table using the product's custom.game_slug metafield.
GET /api/get-product-variants/:handle
Fetches product variants and options only (lightweight alternative to the full product endpoint).
File: app/routes/api.get-product-variants.$handle.ts
| Property | Details |
|---|---|
| Methods | GET (loader) |
| Authentication | None |
| Rate Limiting | None |
Response: Returns { variants, options, handle } — variant pricing, availability, and product options from Shopify Storefront API.
GET /api/predictive-search
Provides search autocomplete results from Shopify's Predictive Search API.
File: app/routes/($locale).api.predictive-search.tsx
| Property | Details |
|---|---|
| Methods | GET (loader) |
| Authentication | None |
| Rate Limiting | None |
Query Parameters:
| Parameter | Default | Description |
|---|---|---|
q | (empty) | Search term |
limit | 10 | Max results |
type | ANY | Comma-separated types (currently only PRODUCT) |
Response: Normalized search results with product data including lottery-specific metafields (game_slug, lottery_ticket_multiplier, lottery_pool_cutoff_time).
Caching: 60 seconds when a search term is provided; 3600 seconds for empty queries.
Utility API Endpoints
POST /api/cleanup-pending-transactions
Expires stale pending transactions to prevent rate limit blockage. Designed for automated cron jobs.
File: app/routes/api.cleanup-pending-transactions.ts
| Property | Details |
|---|---|
| Methods | POST (action), OPTIONS (loader) |
| Authentication | Bearer token (CLEANUP_SECRET_TOKEN env var) |
| Rate Limiting | None (token-protected) |
Request:
curl -X POST https://your-domain.com/api/cleanup-pending-transactions \
-H "Authorization: Bearer YOUR_CLEANUP_SECRET_TOKEN"Response (200):
{
"success": true,
"data": {
"expiredCount": 3,
"expirationTime": "2025-01-15T12:00:00Z"
}
}Behavior: Expires pending payment_transactions older than 60 minutes by updating their status from pending to expired. Uses constant-time string comparison for token validation to prevent timing attacks.
POST /api/seed-transactions
Seeds dummy transaction data for development and testing.
File: app/routes/api.seed-transactions.ts
| Property | Details |
|---|---|
| Methods | POST (action), OPTIONS |
| Authentication | None |
| Rate Limiting | None |
DANGER
Development/testing endpoint only. Should be disabled or protected in production environments.
Query Parameters:
| Parameter | Required | Description |
|---|---|---|
email | No | Target email for transactions (default: generates for multiple dummy users) |
Behavior:
- Without
email: Creates 10 transactions for various dummy users - With
email: Creates 5-6 transactions for the specified user
Infrastructure Endpoints
POST /api/:version/graphql.json
Proxies requests to the Shopify Storefront API. Used by Shopify's checkout system.
File: app/routes/($locale).api.$version.[graphql.json].tsx
| Property | Details |
|---|---|
| Methods | POST (action) |
| Authentication | Passed through from client headers |
| Rate Limiting | Shopify's built-in limits |
Behavior: Forwards the request body and headers directly to https://{PUBLIC_CHECKOUT_DOMAIN}/api/{version}/graphql.json and returns the response as-is.
GET /robots.txt
Generates a dynamic robots.txt with Shopify-standard disallow rules.
File: app/routes/[robots.txt].tsx
| Property | Details |
|---|---|
| Methods | GET (loader) |
| Caching | 24 hours (max-age=86400) |
Disallow Rules Include: /admin, /cart, /orders, /checkouts/, /account, collection sort/filter parameters, preview parameters, and search pages.
Bot-Specific Rules:
adsbot-google— Checkout/order restrictionsNutch— Fully blockedAhrefsBot/AhrefsSiteAudit— 10-second crawl delayMJ12bot— 10-second crawl delayPinterest— 1-second crawl delay
GET /sitemap.xml
Returns the sitemap index using Shopify Hydrogen's built-in getSitemapIndex.
File: app/routes/($locale).[sitemap.xml].tsx
| Property | Details |
|---|---|
| Methods | GET (loader) |
| Caching | 24 hours (max-age=86400) |
GET /sitemap/:type/:page.xml
Returns paginated sitemap pages with locale support.
File: app/routes/($locale).sitemap.$type.$page[.xml].tsx
| Property | Details |
|---|---|
| Methods | GET (loader) |
| Caching | 24 hours (max-age=86400) |
| Locales | EN-US, EN-CA, FR-CA |
URL Pattern: Links are generated as {baseUrl}/{locale}/{type}/{handle} for localized content, or {baseUrl}/{type}/{handle} when no locale is specified.
Key Page Routes
The following are the primary user-facing page routes. All use the ($locale). prefix for i18n support.
Public Pages
| Route File | URL Path | Purpose |
|---|---|---|
($locale)._index.tsx | / | Homepage |
($locale).collections._index.tsx | /collections | All collections listing |
($locale).collections.$handle.tsx | /collections/:handle | Single collection page |
($locale).collections.all.tsx | /collections/all | All products |
($locale).products.$handle.tsx | /products/:handle | Product detail page |
($locale).cart.tsx | /cart | Shopping cart |
($locale).search.tsx | /search | Search results |
($locale).pages.faq.tsx | /pages/faq | FAQ page |
($locale).pages.how-to-play.tsx | /pages/how-to-play | How to play guide |
($locale).pages.about-us.tsx | /pages/about-us | About page |
($locale).pages.contact.tsx | /pages/contact | Contact page |
($locale).pages.past-drawings.tsx | /pages/past-drawings | Past lottery drawings |
($locale).pages.games-schedule.tsx | /pages/games-schedule | Game schedule |
Game Pages
| Route File | URL Path | Purpose |
|---|---|---|
($locale).pages.game-detail.tsx | /pages/game-detail | Lottery game detail |
($locale).pages.game-pay.tsx | /pages/game-pay | Game payment page |
($locale).pages.scratch-collection.tsx | /pages/scratch-collection | Scratch card collection |
($locale).pages.scratch-detail.tsx | /pages/scratch-detail | Scratch card detail |
Account Pages (Authenticated)
| Route File | URL Path | Purpose |
|---|---|---|
($locale).account.tsx | /account | Account layout wrapper |
($locale).account._index.tsx | /account | Account dashboard |
($locale).account.profile.tsx | /account/profile | Profile management |
($locale).account.addresses.tsx | /account/addresses | Address management |
($locale).account.orders._index.tsx | /account/orders | Order history |
($locale).account.orders.$id.tsx | /account/orders/:id | Order detail |
($locale).account.load-credit.tsx | /account/load-credit | Load UBL credits |
($locale).account.payment-history.tsx | /account/payment-history | Payment history |
Auth Pages
| Route File | URL Path | Purpose |
|---|---|---|
($locale).account_.login.tsx | /account/login | Login page |
($locale).account_.logout.tsx | /account/logout | Logout handler |
($locale).account_.authorize.tsx | /account/authorize | OAuth callback |
Other Pages
| Route File | URL Path | Purpose |
|---|---|---|
($locale).blogs._index.tsx | /blogs | Blog listing |
($locale).blogs.$blogHandle._index.tsx | /blogs/:blogHandle | Blog posts |
($locale).blogs.$blogHandle.$articleHandle.tsx | /blogs/:blogHandle/:articleHandle | Blog article |
($locale).policies._index.tsx | /policies | Policies listing |
($locale).policies.$handle.tsx | /policies/:handle | Policy page |
($locale).discount.$code.tsx | /discount/:code | Discount code handler |
($locale).offline.tsx | /offline | PWA offline fallback |
($locale).$.tsx | /* | Catch-all 404 page |
Error Response Conventions
All API endpoints follow consistent error response patterns:
{
"success": false,
"error": "Human-readable error message",
"code": "MACHINE_READABLE_CODE"
}Common HTTP Status Codes
| Code | Meaning | Used By |
|---|---|---|
| 200 | Success | All endpoints |
| 201 | Created | wallet-transactions POST, seed-transactions |
| 400 | Bad Request / Validation Error | All POST endpoints |
| 401 | Unauthorized | Webhook endpoints, checkout, moonpay-sign |
| 403 | Forbidden | Webhook (IP whitelist), checkout (CSRF) |
| 405 | Method Not Allowed | Wrong HTTP method |
| 409 | Conflict | Webhook (replay detection) |
| 413 | Payload Too Large | checkout, wallet-transactions |
| 415 | Unsupported Media Type | checkout (wrong Content-Type) |
| 429 | Too Many Requests | Invoice, webhook, wallet-transactions |
| 500 | Internal Server Error | Unhandled errors |
| 502 | Bad Gateway | Plisio API failures |
| 503 | Service Unavailable | Missing Supabase config, global rate limit |