# 集成应用收款

> 在 Shoplazza 应用里完成向商家收费：先在开发者后台配置定价，再调用 Billing API 处理一次性、周期性与用量收费。

## 前置需求

你已经有一个可运行的应用（参见[开发独立应用](/docs/app/getting-started/create-public-app/develop-app)或[开发嵌入式应用](/docs/app/getting-started/create-public-app/embed-in-admin)），并且想向商家收费。这一步是可选的——如果应用是免费的可以跳过。

:::note
测试店铺或开发者店铺不支持 Billing 支付。请在开发和测试阶段将其设置为 test billing。
:::

## 1.定价详情配置

在 [Developer Platform](https://partners.shoplazza.com/) 中，您可以编辑 APP Listing 来添加套餐并提交审核。目前支持 **free**、**free to install**、**one-time** 和 **recurring charging** 模式。每个 APP 只能有一种收费模式；一旦设置了某种收费类型的套餐，**之后就不能再设置其他类型的套餐**。

### 一次性收费 {#one-time-charge}

操作路径：[Developer Platform](https://partners.shoplazza.com/) --> Apps --> App Display List [Management List] --> Edit listing。

![](https://cnres.appracle.com/c181267558021da5957dc9086e006a80.png)

![](https://cnres.appracle.com/b94ee743b37aab4949f8cbd63e9917e1.png)

![](https://cnres.appracle.com/f3ad5b2e8b434862d97cb65b44e3a91f.png)

![](https://cnres.appracle.com/a4d1115b7f20d0d2adb2ee52287cef03.png)

### 周期性收费 {#recurring-charge}

操作路径：[Developer Platform](https://partners.shoplazza.com/) --> Apps --> App Display List [Management List] --> Edit listing。

#### 仅周期性收费 {#recurring-charge-only}

* **free trial config**

![](https://cnres.appracle.com/e127e7a3c9e63151bea9706ad45c86c1.png)

“trial period” 表示在此期间不会收费。

#### 周期性收费与按量收费 {#recurring-charge--usage-charge}

* _free trial config_

“trial period” 表示在此期间不会收费。

![](https://cnres.appracle.com/4f8839b56c1b0265aa31c28c7d509ef7.png)

* **Add plan and Usage charge Config**

:::note
Usage charges 只能应用于 recurring charging 模式下。附加费用是指增加用量所产生的费用。
:::

1. **Subscription Only**

2. **Subscription + Usage**

## 2.App 调用 Billing API

### 一次性收费 {#one-time-charge-1}

#### 处理时间线

![](https://cnres.appracle.com/918bc5285310c087fac03bdf0b2727c2.png)

#### 详情

1. 商家在 APP 内选择 “one-time” 订阅。
2. APP 调用 [OpenAPI](/api/recurring-application-charge) 创建 one-time charge。

   1. 创建 [webhook](/api/webhook-create)，用于跟踪一次性账单的状态变化。
      对于每个安装 APP 的商家，都会创建一个 webhook。随后，可以通过处理该 webhook 的事件来判断商家的订阅状态。

      ```go
      // 在商家完成 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. 创建 one-time charge

:::note
金额必须与 APP SetUp 中设置的金额一致。
:::

```go
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
}
```

3. Shoplazza 会返回 confirmation_url。
4. APP 展示 confirmation_url 页面。

   ![](https://cnres.appracle.com/c3a5a819af21d20e0569e8b9de1d3d08.png)
5. 商家确认订阅并发起支付。
6. Shoplazza 从商家账户中扣款，并将款项收取到 Shoplazza 账户。
7. webhook 会通知 APP 此实例的订阅信息。
   1. 处理 [Webhook](/docs/app/building-blocks/webhooks/supported-webhook-events/app/purchases_one_time_update) 通知。
      当 Billing 账单状态发生变化时，APP 会通过 webhook 收到通知。APP 可以根据收到的 webhook event 判断商家的订阅状态。
   ```go
   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)
   }
   ```
8. 重定向到 **return_url**（用户在 **confirmation_url** 上确认后被重定向到的页面）。

### 仅周期性收费 {#recurring-charge-only-1}

#### 处理时间线

![](https://cnres.appracle.com/4f01839d944f89c6e7d3a3f4dada7e69.png)

#### 详情

1. 商家在 APP 内选择 “recurring” 订阅套餐。
2. APP 调用 [OpenAPI](/api/recurring-application-charge-create) 创建 recurring charge。

   1. 创建 webhook，用于跟踪 recurring charge 的状态变化。  
      对于每个安装 APP 的商家，都会创建一个 webhook。随后，可以通过处理该 webhook 的事件来判断商家的订阅状态。

      ```go
      // 在商家完成 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. 创建 recurring charge

      :::note
      套餐价格必须与 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
}
```

3. Shoplazza 会返回 confirmation_url。
4. APP 展示 confirmation_url 页面。

![](https://cnres.appracle.com/c3a5a819af21d20e0569e8b9de1d3d08.png)

5. 商家确认订阅并发起支付。
6. Shoplazza 从商家账户中扣款，并将款项收取到 Shoplazza 账户。
7. [webhook](/docs/app/building-blocks/webhooks/supported-webhook-events/app/subscriptions_update) 会通知 APP 此实例的订阅信息。
   1. 处理 Webhook Notifications。当 Billing 账单状态发生变化时，APP 会通过 webhook 收到通知。APP 可以根据收到的 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)
   }
   ```
   
8. 重定向到 return_url（用户在 confirmation_url 上确认后被重定向到的页面）。
9. Shoplazza 会自动计算并扣除下一周期的款项。
10. webhook 会向 APP 推送订阅状态新变化的信息。

### 周期性收费叠加按量收费与免费安装叠加按量收费 {#recurring-charge-with-usage-charge--free-installation-with-usage-charge}

:::tip
  ### Recurring Charge with Usage Charge 与 Free to Install with Usage Charge 对比

  这两种模式本质上相似，主要区别如下：

  1. **Free to Install** 没有基础月费，月度基础成本为 `$0`。
  2. **Free to Install** 允许通过 `charge_interval_days` 自定义计费周期。
:::

#### Recurring charge with Usage Charge 处理时间线

![](https://cnres.appracle.com/7c81581cbf8659910ae7232a177f8c41.png)

#### 详情

1. 商家在 APP 内选择 “recurring” 订阅套餐。
2. APP 调用 OpenAPI 创建 recurring charge。

   1. 创建 webhook，用于跟踪 recurring charge 的状态变化。  
      对于每个安装 APP 的商家，都会创建一个 webhook。随后，可以通过处理该 webhook 的事件来判断商家的订阅状态。

      ```go
      // 在商家完成 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. 创建 recurring charge

      :::note
      套餐价格必须与 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
}
```

3. Shoplazza 会返回 confirmation_url。
4. APP 展示 confirmation_url 页面。

![](https://cnres.appracle.com/c3a5a819af21d20e0569e8b9de1d3d08.png)

5. 商家确认订阅并发起支付。

6. Shoplazza 从商家账户中扣款，并将款项收取到 Shoplazza 账户。

7. webhook 会通知 APP 此实例的订阅信息。
   1. 处理 Webhook Notifications。当 Billing 账单状态发生变化时，APP 会通过 webhook 收到通知。APP 可以根据收到的 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)
   }
   ```

8. 重定向到 return_url（用户在 confirmation_url 上确认后被重定向到的页面）。

9. 在一个订阅周期内，如果产生 usage fees，则创建 Usage charge（可以创建多笔）。

   1. 创建 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
      }
      ```

10. Shoplazza 会自动汇总当前周期的总 usage fees 以及下一周期的订阅费，并向商家扣款。

11. webhook 会向 APP 推送订阅状态新变化的信息。

#### Free installation with Usage Charge 处理时间线

![](https://cnres.appracle.com/7c81581cbf8659910ae7232a177f8c41.png)

#### 详情

1. 商家在 APP 内选择 extra charge（one-time services）。
2. APP 调用 OpenAPI 创建 free recurring charge。

   1. 创建 webhook，用于跟踪 recurring charge 的状态变化。  
      对于每个安装 APP 的商家，都会创建一个 webhook。随后，可以通过处理该 webhook 的事件来判断商家的订阅状态。

      ```go
      // 在商家完成 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. 创建 free recurring charge

      :::note
      套餐价格必须与 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,      //套餐价格，当模式为free to intall时，价格设置为“0” 
          "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
}
```

3. Shoplazza 会返回 confirmation_url。
4. APP 展示 confirmation_url 页面。
5. 商家确认订阅并发起支付。
6. Shoplazza 从商家账户中扣款，并将款项收取到 Shoplazza 账户。
7. webhook 会通知 APP 此实例的订阅信息。
   1. 处理 Webhook Notifications。当 Billing 账单状态发生变化时，APP 会通过 webhook 收到通知。APP 可以根据收到的 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)
   }
   ```
8. 重定向到 return_url（用户在 confirmation_url 上确认后被重定向到的页面）。
9. 在一个订阅周期内，如果产生 usage fees，则创建 Usage charge（可以创建多笔）。

   1. 创建 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
      }
      ```
10. Shoplazza 会自动汇总当前周期的总 usage fees 以及下一周期的订阅费，并向商家扣款。
11. webhook 会向 APP 推送订阅状态新变化的信息。

### 补充：通过 BillingAPI 事件触发 Webhook

在以下状态变化时，会触发 webhook 通知 app。

![](https://cnres.appracle.com/c5652003f582216e808ef8b56b9fec91.png)

## 3.开发者结算指南

已集成 [Billing API ](/docs/app/building-blocks/billing/overview)的 app 可以在 [Developer Platform](https://partners.shoplazza.com/) 查看收入明细，开发者可以在此结算 app 收益。

### 收入审核

当 app 通过 billing 产生收入时，您可以在如下图所示页面查看 app 的收入明细。

![](https://cnres.appracle.com/d0ec52c1be6e7292e46c80dce3653a37.png)

点击 “Details” 查看结算账单详情后，您可以查看该账单周期内的所有交易明细：

![](https://cnres.appracle.com/821e1f8afa3a13882d615bc582d43956.png)

点击交易上的 “Details”，查看该笔交易的具体收费明细。

![](https://cnres.appracle.com/d5dffd6cbfe00c4579ee8b182adc3b1b.png)

### 提现收益

选择可提现类别，并确认账单明细无误后，点击 “Withdraw”。这会向 Shoplazza 发送审核请求。审核通过后，收益将按约定方式到账。

![](https://cnres.appracle.com/9ea7935421d00a6df51ecfe5bf42ba5b.png)

在可提现页面，您可以查看提现进度。

![](https://cnres.appracle.com/7fcd51253805d3797fc5a86441cc6a13.png)

### 查看历史收益

在 “Withdrawn” 类别中，您可以查看此前已提现并完成的结算账单。您还可以查看具体的付款凭证。

![](https://cnres.appracle.com/fd8be46d0442d6abd392fb5e69688dce.png)

## 下一步

完成计费接入后，继续阅读[测试应用](/docs/app/getting-started/create-public-app/test-app)，在开发店铺里验证完整流程，再提交审核。
