How to integrate with the Billing API

Test stores or developer stores do not support payment Billing. Please set it to test billing during the development and testing phases.

1.Pricing details Config

In the developer platform, you can edit the APP Listing to add packages and submit for review. Currently, it supports free, one-time, and recurring charging models. Each APP can only have one type of charging model, and once a package of a certain charging type is set, you cannot set other types of packages afterward.

One-time charge

Operation Path: Developer Platform --> apps --> App Display List [Management List] --> Edit listing.

Recurring charge

Operation Path: Developer Platform --> apps --> App Display List [Management List] --> Edit listing.

Recurring Charge Only

  1. free trial config

The term "trial period" means that there will be no charge during this time.

  1. Add plan

Recurring Charge & Usage Charge

  1. free trial config

The term "trial period" means that there will be no charge during this time.

  1. Add plan and Usage charge Config

Usage charges can only be applied under a recurring charging model.

Additional charges refer to the fees for adding usage.

2.App Call the Billing API

One-time Charge

Processing Timeline

Detail

  1. The merchant selects a "one-time" subscription within the APP.

  2. The APP invokes OpenAPI to create a one-time charge.

    1. Create a webhook for tracking the status changes of the one-time invoice.
      For each merchant that installs the APP, a webhook is created. Subsequently, the subscription status of the merchant can be determined by processing the events of this webhook.

      // 在商家完成 app 安装授权后调用该函数创建一次性收费 webhook
      CreateWebhook(form.ShopDomain, "app/purchases_one_time_update", "https://APP-Host/webhook/one_time", token)
      // CreateWebhook
      //  @Description: 
      //  @param shopDomain 商家域名 eg:test.myshoplaza.com
      //  @param event  webhook 名称
      //  @param token  access_token
      func CreateWebhook(shopDomain, event, address, token string) error {
          url := fmt.Sprintf("https://%s/openapi/2022-07/webhooks", shopDomain)
          method := "POST"
          param := map[string]interface{}{
             "address": address,
             "topic":   event,
          }
      
          payload, err := json.Marshal(param)
          if err != nil {
             return err
          }
      
          client := &http.Client{}
          req, err := http.NewRequest(method, url, bytes.NewReader(payload))
      
          if err != nil {
             return err
          }
          req.Header.Add("Access-Token", token)
          req.Header.Add("Content-Type", "application/json")
      
          res, err := client.Do(req)
          if err != nil {
             return err
          }
          
          defer res.Body.Close()
          
          body, err := ioutil.ReadAll(res.Body)
          if err != nil {
             return err
          }
          fmt.Println(string(body))
          return nil
      }
      
    2. Create a one-time charge

The amount must be consistent with the amount set in the APP SetUp.

type CreateOneTimeChargeResp struct {
    ID         string `json:"id"`
    AppID      string `json:"application_id"`
    Name       string `json:"name"`
    Price      string `json:"price"`
    ConfirmUrl string `json:"confirm_url"`  //订阅确认链接
    ReturnUrl  string `json:"return_url"`
    Status     string `json:"status"`
    Test       bool   `json:"test"`
    CreatedAt  string `json:"created_at"`
    UpdatedAt  string `json:"updated_at"`
}
// 在订阅套餐的接口中调用,创建一次性账单
func CreateOneTimeCharge(shopDomain, appName, appUrl, token string, price float64) (*CreateOneTimeChargeResp, error) {
    url := fmt.Sprintf("https://%s/openapi/2022-01/application_charges", shopDomain)
    body, err := json.Marshal(map[string]interface{}{
       "application_charge": map[string]interface{}{
          "name":       appName,    //app name
          "price":      price,    //一次性收费的价格,需与套餐设置一致
          "return_url": appUrl,    //完成订阅后,重定向的页面
          "test":       false,    //是否为测试链接
       },
    })
    req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
    req.Header.Add("accept", "application/json")
    req.Header.Add("content-type", "application/json")
    req.Header.Add("access-token", token)

    resp, err := http.DefaultClient.Do(req)

    if err != nil {
       return nil, err
    }

    content, err := ioutil.ReadAll(resp.Body)
    if err != nil {
       return nil, err
    }
    chargeResp := &CreateOneTimeChargeResp{}

    err = json.Unmarshal(content, chargeResp)
    if err != nil {
       return nil, err
    }

    fmt.Println(chargeResp)

    return chargeResp, nil
}
  1. Shoplazza will reponse a confirmation_url

  2. APP shows the confirmation_url page

  3. The merchant confirms the subscription and initiates payment.

  4. Shoplazza deducts the payment from the merchant's account and collects it into the Shoplazza account.

  5. A webhook notifies the APP of the subscription information for this instance.

    1. Handling Webhook Notifications
      When the status of a Billing invoice changes, the APP is notified via a webhook. The APP can determine the merchant's subscription status based on the received webhook event
    type OneTimeChargeEvent struct {
        ID            string     `json:"id"`
        StoreID       int        `json:"store_id"`
        ApplicationID string     `json:"application_id"`
        Name          string     `json:"name"`
        Price         string     `json:"price"`
        Status        string     `json:"status"`
        Test          bool       `json:"test"`
        UpdatedAt     *time.Time `json:"updated_at"`
    }
    // gin 框架 handle 示例
    func OneTimeChargeWebhook(c *gin.Context) {
        body, err := ioutil.ReadAll(c.Request.Body)
        if err != nil {
           c.JSON(http.StatusInternalServerError, err.Error())
           return
        }
        fmt.Printf("Header: %+v\n", c.Request.Header)
        headerHmac := c.GetHeader("X-Shoplazza-Hmac-Sha256")
        if !hmacCheck(headerHmac, clientSecret, body) {
            c.JSON(http.StatusUnauthorized, "hmac verification fails.")
            return
        }
        
        event := &OneTimeChargeEvent{}
        err = json.Unmarshal(body, event)
        if err != nil {
           fmt.Println(err.Error())
           c.JSON(http.StatusInternalServerError, err.Error())
           return
        }
        fmt.Printf("Body: %+v\n", event)
        
        // todo 根据账单状态进行处理
        
        c.JSON(http.StatusOK, event)
    }
    
  6. Redirect to the return_url (the page to which the user is redirected after confirming on the confirmation_url).

Recurring Charge Only

Processing Timeline

Detail

  1. The merchant selects a "recurring" subscription plan within the APP.

  2. The APP invokes OpenAPI to create a recurring charge

    1. Create a webhook for tracking the status changes of the recurring charge.
      For each merchant that installs the APP, a webhook is created. Subsequently, the subscription status of the merchant can be determined by processing the events of this webhook.

      // 在商家完成 app 安装授权后调用该函数创建周期性账单更新 webhook
      CreateWebhook(form.ShopDomain, "app/subscriptions_update", "https://APP-Host/webhook/recurring", token)
      // CreateWebhook
      //  @Description: 
      //  @param shopDomain 商家域名 eg:test.myshoplaza.com
      //  @param event  webhook 名称
      //  @param token  access_token
      func CreateWebhook(shopDomain, event,address, token string) error {
          url := fmt.Sprintf("https://%s/openapi/2022-07/webhooks", shopDomain)
          method := "POST"
          param := map[string]interface{}{
             "address": address,
             "topic":   event,
          }
      
          payload, err := json.Marshal(param)
          if err != nil {
             return err
          }
      
          client := &http.Client{}
          req, err := http.NewRequest(method, url, bytes.NewReader(payload))
      
          if err != nil {
             return err
          }
          req.Header.Add("Access-Token", token)
          req.Header.Add("Content-Type", "application/json")
      
          res, err := client.Do(req)
          if err != nil {
             return err
          }
          
          defer res.Body.Close()
          
          body, err := ioutil.ReadAll(res.Body)
          if err != nil {
             return err
          }
          fmt.Println(string(body))
          return nil
      }
      

    2. Create recurring charge

      The package price must be consistent with the package price set in the APP listing.

type CreateRecurringChargeResp struct {
    ID              string `json:"id"`
    ApplicationID   string `json:"application_id"`
    Name            string `json:"name"`
    Price           string `json:"price"`
    CappedAmount    string `json:"capped_amount"`
    Terms           string `json:"terms"`
    ReturnURL       string `json:"return_url"`
    ConfirmationURL string `json:"confirmation_url"`  //确认链接
    Status          string `json:"status"`
    TrialDays       int    `json:"trial_days"`
    ActivatedOn     string `json:"activated_on"`
    TrialEndsOn     string `json:"trial_ends_on"`
    BillingOn       string `json:"billing_on"`
    CancelledOn     string `json:"cancelled_on"`
    CancelSubOn     string `json:"cancel_sub_on"`
    Test            bool   `json:"test"`
    CreatedAt       string `json:"created_at"`
    UpdatedAt       string `json:"updated_at"`
}
// 在订阅套餐的接口中调用,创建周期性账单
func createRecurringCharge(shopDomain, appName, appUrl, token string, price, cappedAmount float64) (*CreateRecurringChargeResp, error) {
    url := fmt.Sprintf("https://%s/openapi/2022-01/recurring_application_charges", shopDomain)
    body, err := json.Marshal(map[string]interface{}{
       "recurring_application_charge": map[string]interface{}{
          "name":          appName,    //app name
          "price":         price,      //套餐价格,需要与listing中设置的套餐价格一致
          "return_url":    appUrl,     //商家完成订阅之后跳转的页面
          "trial_days":    0,          //试用期天数
          "capped_amount": cappedAmount, //每个周期使用费上限
          "terms":         "terms",    //使用费收费说明
          "test":          false,       //是否为测试账单
       },
    })
    req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
    req.Header.Add("accept", "application/json")
    req.Header.Add("content-type", "application/json")
    req.Header.Add("access-token", token)
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
       return nil, err
    }

    respBody, err := ioutil.ReadAll(resp.Body)
    if err != nil {
       return nil, err
    }

    chargeResp := &CreateRecurringChargeResp{}

    err = json.Unmarshal(respBody, chargeResp)
    if err != nil {
       return nil, err
    }
    fmt.Println(chargeResp)

    return chargeResp, nil
}
  1. Shoplazza will reponse a confirmation_url
  2. APP shows the confirmation_url page

  1. The merchant confirms the subscription and initiates payment.
  2. Shoplazza deducts the payment from the merchant's account and collects it into the Shoplazza account.
  3. A webhook notifies the APP of the subscription information for this instance.
    1. Handling Webhook Notifications , When the status of a Billing invoice changes, the APP is notified via a webhook. The APP can determine the merchant's subscription status based on the received webhook event.
    type RecurringChargeEvent struct {
        ActivatedOn   *time.Time `json:"activated_on"`
        ApplicationID string     `json:"application_id"`
        BillingOn     *time.Time `json:"billing_on"`
        CancelledOn   *time.Time `json:"cancelled_on"`
        CappedAmount  string     `json:"capped_amount"`
        ID            string     `json:"id"`
        Name          string     `json:"name"`
        Price         string     `json:"price"`
        Status        string     `json:"status"`
        StoreID       int        `json:"store_id"`
        Test          bool       `json:"test"`
        TrialDays     int        `json:"trial_days"`
        TrialEndsOn   *time.Time `json:"trial_ends_on"`
        UpdatedAt     *time.Time `json:"updated_at"`
    }
    // gin 框架 handle 示例
    func RecurringChargeWebhook(c *gin.Context) {
        body, err := ioutil.ReadAll(c.Request.Body)
        if err != nil {
           c.JSON(http.StatusInternalServerError, err.Error())
           return
        }
        fmt.Printf("Header: %+v\n", c.Request.Header)
        headerHmac := c.GetHeader("X-Shoplazza-Hmac-Sha256")
        if !hmacCheck(headerHmac, clientSecret, body) {
            c.JSON(http.StatusUnauthorized, "hmac verification fails.")
            return
        }
        event := &RecurringChargeEvent{}
        err = json.Unmarshal(body, event)
        if err != nil {
           fmt.Println(err.Error())
           c.JSON(http.StatusInternalServerError, err.Error())
           return
        }
        fmt.Printf("body: %+v\n", event)
        
        // todo 根据账单状态进行处理
        
        c.JSON(http.StatusOK, event)
    }
    

  4. Redirect to the return_url (the page to which the user is redirected after confirming on the confirmation_url).
  5. Shoplazza automatically calculates and deducts the payment for the next cycle.
  6. The webhook pushes the information about the new changes in the subscription status to the APP.

Recurring charge & Usage Charge

Processing Timeline

Detail

  1. The merchant selects a "recurring" subscription plan within the APP.

  2. The APP invokes OpenAPI to create a recurring charge

    1. Create a webhook for tracking the status changes of the recurring charge.
      For each merchant that installs the APP, a webhook is created. Subsequently, the subscription status of the merchant can be determined by processing the events of this webhook.

      // 在商家完成 app 安装授权后调用该函数创建周期性账单更新 webhook
      CreateWebhook(form.ShopDomain, "app/subscriptions_update", "https://APP-Host/webhook/recurring", token)
      // CreateWebhook
      //  @Description: 
      //  @param shopDomain 商家域名 eg:test.myshoplaza.com
      //  @param event  webhook 名称
      //  @param token  access_token
      func CreateWebhook(shopDomain, event,address, token string) error {
          url := fmt.Sprintf("https://%s/openapi/2022-07/webhooks", shopDomain)
          method := "POST"
          param := map[string]interface{}{
             "address": address,
             "topic":   event,
          }
      
          payload, err := json.Marshal(param)
          if err != nil {
             return err
          }
      
          client := &http.Client{}
          req, err := http.NewRequest(method, url, bytes.NewReader(payload))
      
          if err != nil {
             return err
          }
          req.Header.Add("Access-Token", token)
          req.Header.Add("Content-Type", "application/json")
      
          res, err := client.Do(req)
          if err != nil {
             return err
          }
          
          defer res.Body.Close()
          
          body, err := ioutil.ReadAll(res.Body)
          if err != nil {
             return err
          }
          fmt.Println(string(body))
          return nil
      }
      

    2. Create recurring charge

      The package price must be consistent with the package price set in the APP listing.

type CreateRecurringChargeResp struct {
    ID              string `json:"id"`
    ApplicationID   string `json:"application_id"`
    Name            string `json:"name"`
    Price           string `json:"price"`
    CappedAmount    string `json:"capped_amount"`
    Terms           string `json:"terms"`
    ReturnURL       string `json:"return_url"`
    ConfirmationURL string `json:"confirmation_url"`  //确认链接
    Status          string `json:"status"`
    TrialDays       int    `json:"trial_days"`
    ActivatedOn     string `json:"activated_on"`
    TrialEndsOn     string `json:"trial_ends_on"`
    BillingOn       string `json:"billing_on"`
    CancelledOn     string `json:"cancelled_on"`
    CancelSubOn     string `json:"cancel_sub_on"`
    Test            bool   `json:"test"`
    CreatedAt       string `json:"created_at"`
    UpdatedAt       string `json:"updated_at"`
}
// 在订阅套餐的接口中调用,创建周期性账单
func createRecurringCharge(shopDomain, appName, appUrl, token string, price, cappedAmount float64) (*CreateRecurringChargeResp, error) {
    url := fmt.Sprintf("https://%s/openapi/2022-01/recurring_application_charges", shopDomain)
    body, err := json.Marshal(map[string]interface{}{
       "recurring_application_charge": map[string]interface{}{
          "name":          appName,    //app name
          "price":         price,      //套餐价格,需要与listing中设置的套餐价格一致
          "return_url":    appUrl,     //商家完成订阅之后跳转的页面
          "trial_days":    0,          //试用期天数
          "capped_amount": cappedAmount, //每个周期使用费上限
          "terms":         "terms",    //使用费收费说明
          "test":          false,       //是否为测试账单
       },
    })
    req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
    req.Header.Add("accept", "application/json")
    req.Header.Add("content-type", "application/json")
    req.Header.Add("access-token", token)
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
       return nil, err
    }

    respBody, err := ioutil.ReadAll(resp.Body)
    if err != nil {
       return nil, err
    }

    chargeResp := &CreateRecurringChargeResp{}

    err = json.Unmarshal(respBody, chargeResp)
    if err != nil {
       return nil, err
    }
    fmt.Println(chargeResp)

    return chargeResp, nil
}
  1. Shoplazza will reponse a confirmation_url
  2. APP shows the confirmation_url page

  1. The merchant confirms the subscription and initiates payment.

  2. Shoplazza deducts the payment from the merchant's account and collects it into the Shoplazza account.

  3. A webhook notifies the APP of the subscription information for this instance.

    1. Handling Webhook Notifications , When the status of a Billing invoice changes, the APP is notified via a webhook. The APP can determine the merchant's subscription status based on the received webhook event.
    type RecurringChargeEvent struct {
        ActivatedOn   *time.Time `json:"activated_on"`
        ApplicationID string     `json:"application_id"`
        BillingOn     *time.Time `json:"billing_on"`
        CancelledOn   *time.Time `json:"cancelled_on"`
        CappedAmount  string     `json:"capped_amount"`
        ID            string     `json:"id"`
        Name          string     `json:"name"`
        Price         string     `json:"price"`
        Status        string     `json:"status"`
        StoreID       int        `json:"store_id"`
        Test          bool       `json:"test"`
        TrialDays     int        `json:"trial_days"`
        TrialEndsOn   *time.Time `json:"trial_ends_on"`
        UpdatedAt     *time.Time `json:"updated_at"`
    }
    // gin 框架 handle 示例
    func RecurringChargeWebhook(c *gin.Context) {
        body, err := ioutil.ReadAll(c.Request.Body)
        if err != nil {
           c.JSON(http.StatusInternalServerError, err.Error())
           return
        }
        fmt.Printf("Header: %+v\n", c.Request.Header)
        headerHmac := c.GetHeader("X-Shoplazza-Hmac-Sha256")
        if !hmacCheck(headerHmac, clientSecret, body) {
            c.JSON(http.StatusUnauthorized, "hmac verification fails.")
            return
        }
        event := &RecurringChargeEvent{}
        err = json.Unmarshal(body, event)
        if err != nil {
           fmt.Println(err.Error())
           c.JSON(http.StatusInternalServerError, err.Error())
           return
        }
        fmt.Printf("body: %+v\n", event)
        
        // todo 根据账单状态进行处理
        
        c.JSON(http.StatusOK, event)
    }
    
  4. Redirect to the return_url (the page to which the user is redirected after confirming on the confirmation_url).

  5. Within a subscription period, if there are usage fees, then create a Usage charge (which can be multiple charges).

    1. Create usage charge

      type CreateUsageChargeResp struct {
          ID               string     `json:"id"`
          Price            string     `json:"price"`
          BalanceRemaining string     `json:"balance_remaining"`
          BalanceUsed      string     `json:"balance_used"`
          CreatedAt        *time.Time `json:"created_at"`
          Description      string     `json:"description"`
      }
      
      func createUsageCharge(shopDomain, description, chargeID, token string, price float64) (*CreateUsageChargeResp, error) {
          url := fmt.Sprintf("https://%s/openapi/2022-01/recurring_application_charges/%s/usage_charge", shopDomain, chargeID)
      
          body, err := json.Marshal(map[string]interface{}{
             "usage_charge": map[string]interface{}{
                "description": description,
                "price":       price,
             },
          })
          req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
          req.Header.Add("accept", "application/json")
          req.Header.Add("content-type", "application/json")
          req.Header.Add("access-token", token)
          resp, err := http.DefaultClient.Do(req)
          if err != nil {
             return nil, err
          }
      
          respBody, err := ioutil.ReadAll(resp.Body)
          if err != nil {
             return nil, err
          }
      
          chargeResp := &CreateUsageChargeResp{}
      
          err = json.Unmarshal(respBody, chargeResp)
          if err != nil {
             return nil, err
          }
          fmt.Printf("%+v\n", chargeResp)
          return chargeResp, err
      }
      
  6. Shoplazza automatically consolidates the total usage fees for the current period and the subscription fee for the next period for the merchant and deducts the payment from the merchant.

  7. The webhook pushes the information about the new changes in the subscription status to the APP.

Supplement:Trigger Webhook By BillingAPI Event

It will trigger a webhook to notice app at the following status change.

3.Developer Settlement Guide

Apps that have integrated with the Billing API can view income details on the developer platform, where developers can settle the app's earnings.

Income Review

When an app generates revenue through billing, you can view the app's income details on the page as shown in the figure.

After clicking "Details" to view the details of the settlement bill, you can see all transaction details for that bill's period:

Click "Details" on the transaction to view the breakdown of charges for that specific transaction.

Withdrawing Earnings

Select the withdrawable category, and after confirming that the bill details are correct, click on "Withdraw." This will send a review request to Shoplazza. Once the review is approved, the earnings will be received in the agreed-upon manner.

On the withdrawable page, you can view the withdrawal progress.

View Historical Earnings

In the "Withdrawn" category, you can see the settlement bills that have been previously withdrawn and completed. You can also view the specific proof of payment.