Skip to content

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

PatternExampleURL
api.name.tsapi.plisio-invoice.ts/api/plisio-invoice
api.name.$param.tsapi.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

PropertyDetails
MethodsPOST (action), OPTIONS (loader)
AuthenticationRequired — Shopify Customer Account login (customerAccount.isLoggedIn())
CORSOrigin-restricted via getAllowedOrigin() (same-origin + localhost in dev)
Rate LimitingMax 5 pending transactions per email; max 3 transactions per minute per email

Request Body:

json
{
  "amount": 50,
  "email": "[email protected]",
  "customer_name": "John",
  "customer_lastname": "Doe",
  "customer_id": "gid://shopify/Customer/123"
}

Response (200):

json
{
  "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_transactions table before invoice creation; updated with Plisio's provider_txn_id and provider_invoice_url after 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

PropertyDetails
MethodsGET (loader), POST (action)
AuthenticationHMAC signature verification
Rate LimitingIP-based, per-transaction, and global rate limits

Security Layers:

LayerProtectionResponse on Failure
1IP Whitelisting (if PLISIO_WEBHOOK_IPS configured)403 Forbidden
2Rate Limiting (IP + global)429 / 503
3HMAC Signature Validation401 Unauthorized
4Replay Attack Prevention (nonce-based)409 Conflict
5Transaction Rate Limiting429 Too Many Requests
6Duplicate 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

PropertyDetails
MethodsGET (loader), OPTIONS (action)
AuthenticationRequired — Shopify Customer Account login (customerAccount.isLoggedIn())
CORSOrigin-restricted via getAllowedOrigin() (same-origin + localhost in dev)
Rate LimitingNone

Query Parameters:

ParameterRequiredDescription
txn_idYesPlisio transaction ID

Response (200):

json
{
  "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

PropertyDetails
MethodsPOST (action), OPTIONS (loader)
AuthenticationRequired — Shopify Customer Account login
Rate LimitingIP-based (via Supabase)

Request Body:

json
{
  "url": "https://buy.moonpay.com/?apiKey=...&currencyCode=ETH"
}

Response (200):

json
{
  "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

PropertyDetails
MethodsPOST (action)
AuthenticationHMAC-SHA256 signature via Moonpay-Signature-V2 header
Rate LimitingIP-based (100/min) and global (1000/min)

Security Layers:

LayerProtectionResponse on Failure
1IP rate limiting (100 req/min per IP)429 Too Many Requests
2Global rate limiting (1000 req/min)503 Service Unavailable
3HMAC-SHA256 signature verification (timestamp freshness + constant-time comparison)401 Unauthorized
4Replay attack prevention (nonce-based via checkGenericWebhookNonce())200 Duplicate acknowledged
5Payload structure validation + security event logging200 (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 shared replay-protection.server.ts
  • Stores nonces in shared webhook_nonces table 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:

  1. Look up by provider_txn_id (MoonPay's internal ID) — matches previously updated records
  2. Look up by order_number (= externalTransactionId) — matches pre-created pending records from the sign endpoint
  3. If neither found — create a new transaction record

Status Mapping:

MoonPay StatusInternal PaymentStatus
completedcompleted
failedfailed
waitingPayment, waitingAuthorizationprocessing
pendingpending

Payload Validation:

  • Handles MoonPay sending data as 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

PropertyDetails
MethodsPOST (action)
AuthenticationRequired — Shopify Customer Account (handleAuthStatus())
Rate LimitingRelies on Shopify's built-in API rate limits

Request Body:

json
{
  "amount": "50",
  "email": "[email protected]",
  "customerAccessToken": "token"
}

Response (200):

json
{
  "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

PropertyDetails
MethodsGET (loader), POST (action), OPTIONS
AuthenticationRequired — Shopify Customer Account login (customerAccount.isLoggedIn())
CORSOrigin-restricted via getAllowedOrigin() (same-origin + localhost in dev)
Rate LimitingIn-memory: 30 requests per minute per IP

GET — Fetch Transaction History

Fetches transaction history for the authenticated user from payment_transactions.

Query Parameters:

ParameterRequiredDescription
emailOne of email/walletUser email address
wallet_addressOne of email/walletEthereum wallet address (0x...)
limitNoMax results (default: 50)

Response:

json
{
  "transactions": [
    {
      "id": "uuid",
      "user_email": "[email protected]",
      "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:

FieldRequiredTypeValidation
user_emailYesstringEmail format
order_numberYesstringNon-empty
providerYesstringMust be moonpay or plisio
amountYesnumberPositive number
user_nameNostringCustomer first name
user_lastnameNostringCustomer last name
user_idNostringCustomer ID
provider_txn_idNostringProvider's transaction ID
provider_invoice_urlNostringProvider's invoice URL
amount_usdNonumberPositive number
currencyNostringDefaults to USD
crypto_currencyNostringCrypto currency code
wallet_addressNostringEthereum address format (0x + 40 hex chars)
tx_hashNostringTransaction hash
conversion_rateNonumberPositive number
statusNostringOne of: pending, processing, awaiting_confirmation, completed, failed, cancelled, expired, refunded, partially_refunded, error
metadataNoobjectArbitrary JSON metadata
expires_atNostringISO 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

PropertyDetails
MethodsGET (loader)
AuthenticationNone
Rate LimitingNone

URL Parameters:

ParameterDescription
handleShopify 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

PropertyDetails
MethodsGET (loader)
AuthenticationNone
Rate LimitingNone

Response: Returns { variants, options, handle } — variant pricing, availability, and product options from Shopify Storefront API.


Provides search autocomplete results from Shopify's Predictive Search API.

File: app/routes/($locale).api.predictive-search.tsx

PropertyDetails
MethodsGET (loader)
AuthenticationNone
Rate LimitingNone

Query Parameters:

ParameterDefaultDescription
q(empty)Search term
limit10Max results
typeANYComma-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

PropertyDetails
MethodsPOST (action), OPTIONS (loader)
AuthenticationBearer token (CLEANUP_SECRET_TOKEN env var)
Rate LimitingNone (token-protected)

Request:

bash
curl -X POST https://your-domain.com/api/cleanup-pending-transactions \
  -H "Authorization: Bearer YOUR_CLEANUP_SECRET_TOKEN"

Response (200):

json
{
  "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

PropertyDetails
MethodsPOST (action), OPTIONS
AuthenticationNone
Rate LimitingNone

DANGER

Development/testing endpoint only. Should be disabled or protected in production environments.

Query Parameters:

ParameterRequiredDescription
emailNoTarget 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

PropertyDetails
MethodsPOST (action)
AuthenticationPassed through from client headers
Rate LimitingShopify'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

PropertyDetails
MethodsGET (loader)
Caching24 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 restrictions
  • Nutch — Fully blocked
  • AhrefsBot / AhrefsSiteAudit — 10-second crawl delay
  • MJ12bot — 10-second crawl delay
  • Pinterest — 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

PropertyDetails
MethodsGET (loader)
Caching24 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

PropertyDetails
MethodsGET (loader)
Caching24 hours (max-age=86400)
LocalesEN-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 FileURL PathPurpose
($locale)._index.tsx/Homepage
($locale).collections._index.tsx/collectionsAll collections listing
($locale).collections.$handle.tsx/collections/:handleSingle collection page
($locale).collections.all.tsx/collections/allAll products
($locale).products.$handle.tsx/products/:handleProduct detail page
($locale).cart.tsx/cartShopping cart
($locale).search.tsx/searchSearch results
($locale).pages.faq.tsx/pages/faqFAQ page
($locale).pages.how-to-play.tsx/pages/how-to-playHow to play guide
($locale).pages.about-us.tsx/pages/about-usAbout page
($locale).pages.contact.tsx/pages/contactContact page
($locale).pages.past-drawings.tsx/pages/past-drawingsPast lottery drawings
($locale).pages.games-schedule.tsx/pages/games-scheduleGame schedule

Game Pages

Route FileURL PathPurpose
($locale).pages.game-detail.tsx/pages/game-detailLottery game detail
($locale).pages.game-pay.tsx/pages/game-payGame payment page
($locale).pages.scratch-collection.tsx/pages/scratch-collectionScratch card collection
($locale).pages.scratch-detail.tsx/pages/scratch-detailScratch card detail

Account Pages (Authenticated)

Route FileURL PathPurpose
($locale).account.tsx/accountAccount layout wrapper
($locale).account._index.tsx/accountAccount dashboard
($locale).account.profile.tsx/account/profileProfile management
($locale).account.addresses.tsx/account/addressesAddress management
($locale).account.orders._index.tsx/account/ordersOrder history
($locale).account.orders.$id.tsx/account/orders/:idOrder detail
($locale).account.load-credit.tsx/account/load-creditLoad UBL credits
($locale).account.payment-history.tsx/account/payment-historyPayment history

Auth Pages

Route FileURL PathPurpose
($locale).account_.login.tsx/account/loginLogin page
($locale).account_.logout.tsx/account/logoutLogout handler
($locale).account_.authorize.tsx/account/authorizeOAuth callback

Other Pages

Route FileURL PathPurpose
($locale).blogs._index.tsx/blogsBlog listing
($locale).blogs.$blogHandle._index.tsx/blogs/:blogHandleBlog posts
($locale).blogs.$blogHandle.$articleHandle.tsx/blogs/:blogHandle/:articleHandleBlog article
($locale).policies._index.tsx/policiesPolicies listing
($locale).policies.$handle.tsx/policies/:handlePolicy page
($locale).discount.$code.tsx/discount/:codeDiscount code handler
($locale).offline.tsx/offlinePWA offline fallback
($locale).$.tsx/*Catch-all 404 page

Error Response Conventions

All API endpoints follow consistent error response patterns:

json
{
  "success": false,
  "error": "Human-readable error message",
  "code": "MACHINE_READABLE_CODE"
}

Common HTTP Status Codes

CodeMeaningUsed By
200SuccessAll endpoints
201Createdwallet-transactions POST, seed-transactions
400Bad Request / Validation ErrorAll POST endpoints
401UnauthorizedWebhook endpoints, checkout, moonpay-sign
403ForbiddenWebhook (IP whitelist), checkout (CSRF)
405Method Not AllowedWrong HTTP method
409ConflictWebhook (replay detection)
413Payload Too Largecheckout, wallet-transactions
415Unsupported Media Typecheckout (wrong Content-Type)
429Too Many RequestsInvoice, webhook, wallet-transactions
500Internal Server ErrorUnhandled errors
502Bad GatewayPlisio API failures
503Service UnavailableMissing Supabase config, global rate limit

UberLotto Technical Documentation