Skip to main content

Overview

Alipay is a leading mobile payment platform in China. This guide will walk you through the complete process of integrating Alipay H5 payment, including application registration, payment credentials, and configuration in superun.
Please note Alipay payment integration requires users to complete application registration and credential acquisition themselves. superun provides guidance for configuring environment variables. Use Alipay’s sandbox environment when testing.

Step 1: Alipay Open Platform Configuration

1.1 Registration and Login

  1. Visit Alipay Open Platform
  2. Log in with Enterprise Alipay Account (personal accounts cannot apply for payment products)
  3. Complete developer certification (requires business license)

1.2 Create Application

Enter Console

After logging in, click “Console” in the top right → “My Applications” → “Create Application

Select Application Type

TypeDescriptionUse Case
Web ApplicationFor PC/H5 web pagesChoose this
Mobile ApplicationFor iOS/Android native apps-
Mini ProgramFor Alipay Mini Programs-

Fill in Application Information

  • Application Name: e.g., “Travel Puzzle Workshop”
  • Application Icon: Upload application logo (200x200px)
  • Application Description: Brief description of application functionality
  • Application Type: Web Application
Click “Confirm Create” to get APPID (e.g., 2021006128604471)

1.3 Configure Keys (Important)

Download Key Tool

  1. Go to application details page → “Development Settings” → “Interface Signature Method
  2. Click “Set” → Download Alipay Key Generation Tool

Generate Key Pair

Open the key tool:
  1. Key Format: Select PKCS8 (Java compatible)
  2. Key Length: Select RSA2 (2048)
  3. Click “Generate Key
The tool will generate two files:
  • Application Public Key.txt - Upload to Alipay
  • Application Private Key.txt - Save securely, configure on your server

Upload Public Key to Get Alipay Public Key

  1. Return to Alipay Open Platform → “Interface Signature Method” → “Set
  2. Signature Mode: Select “Public Key
  3. Fill Application Public Key: Copy and paste content from Application Public Key.txt
  4. Click “Save Settings
Important: After saving, the page will display “Alipay Public Key”, click “View” and copy it. This public key is different from the application public key you generated and is used to verify callback signatures.

Three Keys and Their Uses

KeySourcePurposeStorage Location
Application Private KeyYou generateSign requestsALIPAY_PRIVATE_KEY
Application Public KeyYou generateUpload to AlipayAlipay backend
Alipay Public KeyProvided by AlipayVerify callback signaturesALIPAY_PUBLIC_KEY

1.4 Bind Payment Products

Enter Product Binding

Application details page → Left menu “Callable Products” → In the product list, select “Payment” → “PC Website Payment” or “Mobile Website Payment” → Clicking will open the product details page in a new tab, where you can perform the binding operation

Select Payment Product

Product NameAPIUse CaseFee Rate
PC Website Paymentalipay.trade.page.payPC web payment0.6%
Mobile Website Paymentalipay.trade.wap.payMobile H50.6%
APP Paymentalipay.trade.app.payNative App0.6%
Select according to your scenario, click “Bind

Sign Product Contract

After binding, merchant contract is required:
  1. Click “Sign” next to the product
  2. Fill in merchant information (business license, legal representative info, etc.)
  3. Submit for review (1-3 business days)
  4. Product becomes available after approval

1.5 Configure Callback Address

Set Authorization Callback

Application details page → “Development Settings” → “Authorization Callback Address Fill in your domain (e.g., https://your-domain.com)

Interface Content Encryption (Optional)

For higher security, you can enable AES encryption:
  1. Development Settings” → “Interface Content Encryption Method” → “Set
  2. Select “AES Key
  3. Click “Generate AES Key” and save

1.6 Application Launch

Submit for Review

Application details page → Click “Submit for Review Fill in review information:
  • Application website (must be ICP registered)
  • Test account (if any)
  • Application description

Review Period

Usually 1-3 business days Status changes to “Launched” after approval
Note: Application launch ≠ Product available, products need separate contracts

1.7 Final Configuration Checklist

After completing the above steps, you need to save the following information:
# Alipay Configuration
ALIPAY_APP_ID=2021006128604471
ALIPAY_PRIVATE_KEY=MIIEvgIBADANBgkqhkiG9w0BAQEFAASC...(long private key)
ALIPAY_PUBLIC_KEY=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...(Alipay public key)

Step 2: Database Design

Create the following tables in superun Cloud:
-- Users table
CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  device_id TEXT UNIQUE NOT NULL,
  created_at TIMESTAMPTZ DEFAULT now()
);

-- Membership plans table
CREATE TABLE membership_plans (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  price_cents INTEGER NOT NULL,
  duration_days INTEGER NOT NULL,
  description TEXT,
  is_active BOOLEAN DEFAULT true,
  sort_order INTEGER DEFAULT 0
);

-- Orders table
CREATE TABLE orders (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id),
  plan_id UUID REFERENCES membership_plans(id),
  order_no TEXT UNIQUE NOT NULL,
  amount_cents INTEGER NOT NULL,
  status TEXT DEFAULT 'pending', -- pending/paid/cancelled
  wechat_transaction_id TEXT,    -- Reuse to store Alipay transaction ID
  paid_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT now()
);

-- User memberships table
CREATE TABLE user_memberships (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES users(id),
  plan_id UUID REFERENCES membership_plans(id),
  start_at TIMESTAMPTZ NOT NULL,
  expire_at TIMESTAMPTZ NOT NULL,
  is_active BOOLEAN DEFAULT true
);

Step 3: Edge Function Implementation

3.1 Create Order (alipay-create-order)

Create function alipay-create-order in Build → Services → Edge Functions:
import { serve } from "https://deno.land/[email protected]/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/[email protected]";

const corsHeaders = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
};

// Generate order number
function generateOrderNo(): string {
  const now = new Date();
  const dateStr = now.toISOString().replace(/[-:T.Z]/g, '').slice(0, 14);
  const random = Math.random().toString(36).substring(2, 8).toUpperCase();
  return `A${dateStr}${random}`;
}

// Format private key (add PEM headers)
function formatPrivateKey(privateKey: string): string {
  let key = privateKey.trim();
  if (key.includes('-----BEGIN')) return key;
  return `-----BEGIN RSA PRIVATE KEY-----\n${key}\n-----END RSA PRIVATE KEY-----`;
}

// RSA2 signature
async function signWithRSA(content: string, privateKeyPem: string): Promise<string> {
  const formattedKey = formatPrivateKey(privateKeyPem);
  
  const pemContents = formattedKey
    .replace(/-----BEGIN RSA PRIVATE KEY-----/g, '')
    .replace(/-----END RSA PRIVATE KEY-----/g, '')
    .replace(/-----BEGIN PRIVATE KEY-----/g, '')
    .replace(/-----END PRIVATE KEY-----/g, '')
    .replace(/\s/g, '');
  
  const binaryDer = Uint8Array.from(atob(pemContents), c => c.charCodeAt(0));
  
  const privateKey = await crypto.subtle.importKey(
    'pkcs8',
    binaryDer,
    { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
    false,
    ['sign']
  );
  
  const data = new TextEncoder().encode(content);
  const signature = await crypto.subtle.sign('RSASSA-PKCS1-v1_5', privateKey, data);
  
  return btoa(String.fromCharCode(...new Uint8Array(signature)));
}

// Build sign string (parameters sorted by ASCII)
function buildSignString(params: Record<string, string>): string {
  const sortedKeys = Object.keys(params).sort();
  const pairs = sortedKeys
    .filter(key => params[key] !== '' && params[key] !== undefined && key !== 'sign')
    .map(key => `${key}=${params[key]}`);
  return pairs.join('&');
}

serve(async (req) => {
  if (req.method === "OPTIONS") {
    return new Response(null, { headers: corsHeaders });
  }

  try {
    const { plan_id, device_id, return_url } = await req.json();

    if (!plan_id || !device_id) {
      return new Response(
        JSON.stringify({ error: "Missing required parameters" }),
        { headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 400 }
      );
    }

    // Initialize Supabase
    const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
    const supabaseKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
    const supabase = createClient(supabaseUrl, supabaseKey);

    // Get Alipay configuration
    const appId = Deno.env.get("ALIPAY_APP_ID");
    const privateKey = Deno.env.get("ALIPAY_PRIVATE_KEY");

    if (!appId || !privateKey) {
      return new Response(
        JSON.stringify({ error: "Payment configuration incomplete" }),
        { headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 500 }
      );
    }

    // Get or create user
    let { data: user } = await supabase
      .from("users")
      .select("id")
      .eq("device_id", device_id)
      .single();

    if (!user) {
      const { data: newUser } = await supabase
        .from("users")
        .insert({ device_id })
        .select("id")
        .single();
      user = newUser;
    }

    // Get plan
    const { data: plan } = await supabase
      .from("membership_plans")
      .select("*")
      .eq("id", plan_id)
      .eq("is_active", true)
      .single();

    if (!plan) {
      return new Response(
        JSON.stringify({ error: "Plan not found" }),
        { headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 400 }
      );
    }

    // Create order
    const orderNo = generateOrderNo();
    const { data: order } = await supabase
      .from("orders")
      .insert({
        user_id: user.id,
        plan_id: plan.id,
        order_no: orderNo,
        amount_cents: plan.price_cents,
        status: "pending",
      })
      .select()
      .single();

    // Build Alipay request parameters
    const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 19);
    const amount = (plan.price_cents / 100).toFixed(2);
    
    const bizContent = {
      out_trade_no: orderNo,
      total_amount: amount,
      subject: `Membership Subscription-${plan.name}`,
      product_code: 'FAST_INSTANT_TRADE_PAY', // PC website payment
      // product_code: 'QUICK_WAP_WAY',       // Mobile website payment
    };

    const params: Record<string, string> = {
      app_id: appId,
      method: 'alipay.trade.page.pay',  // PC website payment
      // method: 'alipay.trade.wap.pay', // Mobile website payment
      format: 'JSON',
      charset: 'utf-8',
      sign_type: 'RSA2',
      timestamp: timestamp,
      version: '1.0',
      biz_content: JSON.stringify(bizContent),
      notify_url: `${supabaseUrl}/functions/v1/alipay-notify`,
    };
    
    if (return_url) {
      params.return_url = return_url;
    }

    // Generate signature
    const signString = buildSignString(params);
    const sign = await signWithRSA(signString, privateKey);
    params.sign = sign;

    // Build payment URL
    const gatewayUrl = 'https://openapi.alipay.com/gateway.do';
    const payUrl = `${gatewayUrl}?${new URLSearchParams(params).toString()}`;

    return new Response(
      JSON.stringify({
        success: true,
        order_id: order.id,
        order_no: orderNo,
        amount: plan.price_cents,
        plan_name: plan.name,
        pay_url: payUrl,
      }),
      { headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 200 }
    );

  } catch (error) {
    console.error("[alipay-create-order] Error:", error);
    return new Response(
      JSON.stringify({ error: error.message || "Server error" }),
      { headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 500 }
    );
  }
});

3.2 Payment Callback (alipay-notify)

Create function alipay-notify to handle payment callbacks:
import { serve } from "https://deno.land/[email protected]/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/[email protected]";

// Format public key
function formatPublicKey(publicKey: string): string {
  let key = publicKey.trim();
  if (key.includes('-----BEGIN')) return key;
  return `-----BEGIN PUBLIC KEY-----\n${key}\n-----END PUBLIC KEY-----`;
}

// RSA2 signature verification
async function verifySignature(content: string, sign: string, publicKeyPem: string): Promise<boolean> {
  try {
    const formattedKey = formatPublicKey(publicKeyPem);
    
    const pemContents = formattedKey
      .replace(/-----BEGIN PUBLIC KEY-----/g, '')
      .replace(/-----END PUBLIC KEY-----/g, '')
      .replace(/\s/g, '');
    
    const binaryDer = Uint8Array.from(atob(pemContents), c => c.charCodeAt(0));
    
    const publicKey = await crypto.subtle.importKey(
      'spki',
      binaryDer,
      { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
      false,
      ['verify']
    );
    
    const data = new TextEncoder().encode(content);
    const signatureBytes = Uint8Array.from(atob(sign), c => c.charCodeAt(0));
    
    return await crypto.subtle.verify('RSASSA-PKCS1-v1_5', publicKey, signatureBytes, data);
  } catch (error) {
    console.error("[alipay-notify] Verify error:", error);
    return false;
  }
}

// Build verification string (exclude sign and sign_type)
function buildVerifyString(params: Record<string, string>): string {
  const sortedKeys = Object.keys(params).sort();
  const pairs = sortedKeys
    .filter(key => params[key] !== '' && key !== 'sign' && key !== 'sign_type')
    .map(key => `${key}=${params[key]}`);
  return pairs.join('&');
}

// Parse form data
function parseFormData(body: string): Record<string, string> {
  const params: Record<string, string> = {};
  const pairs = body.split('&');
  for (const pair of pairs) {
    const [key, value] = pair.split('=');
    if (key && value !== undefined) {
      params[decodeURIComponent(key)] = decodeURIComponent(value.replace(/\+/g, ' '));
    }
  }
  return params;
}

serve(async (req) => {
  try {
    if (req.method !== "POST") {
      return new Response("Method Not Allowed", { status: 405 });
    }

    const alipayPublicKey = Deno.env.get("ALIPAY_PUBLIC_KEY");
    if (!alipayPublicKey) {
      return new Response("fail", { status: 500 });
    }

    // Parse request
    const body = await req.text();
    const params = parseFormData(body);
    const sign = params.sign;
    
    if (!sign) {
      return new Response("fail", { status: 400 });
    }

    // Verify signature
    const verifyString = buildVerifyString(params);
    const isValid = await verifySignature(verifyString, sign, alipayPublicKey);
    
    if (!isValid) {
      console.error("[alipay-notify] Invalid signature");
      return new Response("fail", { status: 400 });
    }

    // Initialize Supabase
    const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
    const supabaseKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
    const supabase = createClient(supabaseUrl, supabaseKey);

    const outTradeNo = params.out_trade_no;
    const tradeNo = params.trade_no;
    const tradeStatus = params.trade_status;

    // Find order
    const { data: order } = await supabase
      .from("orders")
      .select("*, plan:membership_plans(*)")
      .eq("order_no", outTradeNo)
      .single();

    if (!order) {
      return new Response("fail", { status: 404 });
    }

    // Prevent duplicate processing
    if (order.status === "paid") {
      return new Response("success", { status: 200 });
    }

    // Handle payment success
    if (tradeStatus === "TRADE_SUCCESS" || tradeStatus === "TRADE_FINISHED") {
      // Update order
      await supabase
        .from("orders")
        .update({
          status: "paid",
          wechat_transaction_id: tradeNo,
          paid_at: new Date().toISOString(),
        })
        .eq("id", order.id);

      // Create/renew membership
      const durationDays = order.plan?.duration_days || 30;

      const { data: existingMembership } = await supabase
        .from("user_memberships")
        .select("*")
        .eq("user_id", order.user_id)
        .single();

      if (existingMembership) {
        // Renew
        let newExpireAt = new Date(existingMembership.expire_at);
        if (newExpireAt < new Date()) newExpireAt = new Date();
        newExpireAt.setDate(newExpireAt.getDate() + durationDays);

        await supabase
          .from("user_memberships")
          .update({
            plan_id: order.plan_id,
            expire_at: newExpireAt.toISOString(),
            is_active: true,
          })
          .eq("user_id", order.user_id);
      } else {
        // New membership
        const expireAt = new Date();
        expireAt.setDate(expireAt.getDate() + durationDays);
        
        await supabase
          .from("user_memberships")
          .insert({
            user_id: order.user_id,
            plan_id: order.plan_id,
            start_at: new Date().toISOString(),
            expire_at: expireAt.toISOString(),
            is_active: true,
          });
      }

      console.log("[alipay-notify] Payment success:", outTradeNo);
    }

    return new Response("success", { status: 200 });

  } catch (error) {
    console.error("[alipay-notify] Error:", error);
    return new Response("fail", { status: 500 });
  }
});

3.3 Query Order (alipay-query-order)

Create function alipay-query-order for active order status queries:
import { serve } from "https://deno.land/[email protected]/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/[email protected]";

const corsHeaders = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
};

// formatPrivateKey, signWithRSA, buildSignString same as above

serve(async (req) => {
  if (req.method === "OPTIONS") {
    return new Response(null, { headers: corsHeaders });
  }

  try {
    const { order_no } = await req.json();

    if (!order_no) {
      return new Response(
        JSON.stringify({ error: "Missing order number" }),
        { headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 400 }
      );
    }

    const appId = Deno.env.get("ALIPAY_APP_ID");
    const privateKey = Deno.env.get("ALIPAY_PRIVATE_KEY");
    const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
    const supabaseKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
    const supabase = createClient(supabaseUrl, supabaseKey);

    // Find local order
    const { data: order } = await supabase
      .from("orders")
      .select("*, plan:membership_plans(*)")
      .eq("order_no", order_no)
      .single();

    if (!order) {
      return new Response(
        JSON.stringify({ error: "Order not found" }),
        { headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 404 }
      );
    }

    // Already paid, return directly
    if (order.status === "paid") {
      return new Response(
        JSON.stringify({ success: true, status: "paid", message: "Order already paid" }),
        { headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 200 }
      );
    }

    // Query Alipay
    const timestamp = new Date().toISOString().replace('T', ' ').slice(0, 19);
    const params: Record<string, string> = {
      app_id: appId!,
      method: 'alipay.trade.query',
      format: 'JSON',
      charset: 'utf-8',
      sign_type: 'RSA2',
      timestamp: timestamp,
      version: '1.0',
      biz_content: JSON.stringify({ out_trade_no: order_no }),
    };

    const signString = buildSignString(params);
    const sign = await signWithRSA(signString, privateKey!);
    params.sign = sign;

    const response = await fetch(
      `https://openapi.alipay.com/gateway.do?${new URLSearchParams(params).toString()}`
    );
    const result = await response.json();
    const queryResponse = result.alipay_trade_query_response;

    const tradeStatus = queryResponse?.trade_status;

    // Payment success - update order and membership
    if (tradeStatus === "TRADE_SUCCESS" || tradeStatus === "TRADE_FINISHED") {
      await supabase
        .from("orders")
        .update({
          status: "paid",
          wechat_transaction_id: queryResponse.trade_no,
          paid_at: new Date().toISOString(),
        })
        .eq("id", order.id);

      // Create/renew membership logic same as notify

      return new Response(
        JSON.stringify({ success: true, status: "paid", message: "Payment successful" }),
        { headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 200 }
      );
    }

    // Other statuses
    return new Response(
      JSON.stringify({ 
        success: true, 
        status: tradeStatus || "unknown",
        message: queryResponse?.sub_msg || "Unknown status"
      }),
      { headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 200 }
    );

  } catch (error) {
    return new Response(
      JSON.stringify({ error: error.message }),
      { headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 500 }
    );
  }
});

Step 4: Frontend Integration

4.1 Create Order and Redirect to Payment

// Create order and redirect to payment
const handlePurchase = async (planId: string) => {
  const result = await createOrder(planId, "alipay");
  
  if (result.pay_url) {
    // Save order number (for query after payment completion)
    localStorage.setItem("pending_order", JSON.stringify({
      orderNo: result.order_no,
      timestamp: Date.now()
    }));
    
    // Redirect to Alipay
    window.location.href = result.pay_url;
  }
};

4.2 Query Result After Payment Completion

// Query result after payment completion
const handleQueryOrder = async () => {
  const stored = localStorage.getItem("pending_order");
  if (!stored) return;
  
  const { orderNo } = JSON.parse(stored);
  const result = await queryAlipayOrder(orderNo);
  
  if (result.status === "paid") {
    localStorage.removeItem("pending_order");
    alert("Payment successful!");
  }
};

Step 5: Configuration Checklist

5.1 Edge Functions Configuration

Configure in supabase/config.toml:
[functions.alipay-create-order]
verify_jwt = false

[functions.alipay-notify]
verify_jwt = false

[functions.alipay-query-order]
verify_jwt = false

5.2 Environment Variables (Secrets)

Set the following environment variables in Build → Services → Alipay Payment:
Variable NameDescription
ALIPAY_APP_IDAlipay application APPID
ALIPAY_PRIVATE_KEYApplication private key (PKCS8 format)
ALIPAY_PUBLIC_KEYAlipay public key
Important Notes
  • Never paste your Alipay keys and private key in chat. Configure them via Build → Services → Alipay Payment using environment variables.
  • Private key file content must be in PKCS8 format.
  • Public key must match the private key, otherwise signature verification errors will occur.

Step 6: API Reference

Product TypeAPI MethodProduct CodeUse Case
PC Website Paymentalipay.trade.page.payFAST_INSTANT_TRADE_PAYPC Web
Mobile Website Paymentalipay.trade.wap.payQUICK_WAP_WAYMobile H5
Order Queryalipay.trade.query-Active status query

Step 7: Testing Process

  1. Create a sandbox application in Alipay Open Platform for testing
  2. Use a 1 cent plan to test the complete payment flow
  3. Check if database order status is updated
  4. Verify if membership status is correctly activated

Step 8: Process Summary

Register Open Platform → Create Application → Get APPID

Generate Key Pair → Upload Application Public Key → Get Alipay Public Key

Bind Payment Product → Complete Contract → Product Available

Submit Application Review → Application Launched → Production Environment Available

Configure to Server → Test Payment Flow → Launch

Step 9: Common Errors

Cause: Product not bound or contract not signed.Solution:
  • Check if application has bound corresponding payment product
  • Check if product contract is completed
  • Check if application is launched
Cause: Key configuration error.Solution:
  • Confirm using PKCS8 format private key
  • Confirm private key has no line breaks or extra spaces
  • Confirm Alipay public key is copied from backend (not the application public key you generated)
Cause: Private key format is incorrect or public key doesn’t match.Solution: Ensure the private key is in PKCS8 format, and the public key is the correct one obtained from Alipay Open Platform.
Cause: Application has not bound the corresponding payment product.Solution: Bind “PC Website Payment” or “Mobile Website Payment” product in the Alipay Open Platform application details page.
Cause: Callback URL is incorrectly configured or public key is incorrect.Solution: Ensure the callback URL is correctly configured as the Edge Function address, and use the correct Alipay public key.
ComparisonMobile Website PaymentPC Website Payment
APIalipay.trade.wap.payalipay.trade.page.pay
Product CodeQUICK_WAP_WAYFAST_INSTANT_TRADE_PAY
Payment InterfaceMobile optimizedPC optimized
H5 UsageRecommendedUsable but experience is average

superun Website

Learn more about product features and examples.