如何通过 API 将 ChargeBee 订阅数据导入 Google Spreadsheet
结论
可以直接通过 Google Apps Script 调用 Chargebee REST API,并将返回的数据写入 Google Spreadsheet,不需要单独部署服务器。
Customer name 和订阅金额可以从订阅及其关联的客户数据中获取。不过,“payment status”通常不属于订阅本身的状态。订阅状态反映订阅是否有效、暂停或取消,付款状态则要根据 Invoice(发票)或 Transaction(交易)数据判断。下面的示例将最新一张发票的状态作为付款状态。
开始前需要准备
开始前需要准备:
- Chargebee Site Name。例如,站点地址为
https://acme.chargebee.com,Site Name 就是acme - Chargebee API Key
- 一个 Google Spreadsheet
- 对客户、订阅和发票拥有读取权限的 Chargebee API Key
不要把 API Key 直接写入共享的 Spreadsheet 单元格或公开脚本。可以将它保存在 Apps Script 的 Script Properties 中。
Chargebee 的测试环境和正式环境使用不同的数据和 API Key,调试时应先使用测试站点。
保存 Chargebee 配置
打开目标 Google Spreadsheet,依次选择:
扩展程序Apps Script- 左侧的“项目设置”
- 在“脚本属性”中添加以下属性
| 属性名 | 示例值 |
|---|---|
CHARGEBEE_SITE | acme |
CHARGEBEE_API_KEY | test_xxxxxxxxxxxxx |
CHARGEBEE_SITE 只需填写站点名称,不要填写完整 URL。
Apps Script 示例
将下面的代码粘贴到 Apps Script 编辑器中:
function importChargebeeSubscriptions() {
const properties = PropertiesService.getScriptProperties();
const site = properties.getProperty('CHARGEBEE_SITE');
const apiKey = properties.getProperty('CHARGEBEE_API_KEY');
if (!site || !apiKey) {
throw new Error(
'请先在 Script Properties 中配置 CHARGEBEE_SITE 和 CHARGEBEE_API_KEY。'
);
}
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheetName = 'Chargebee Subscriptions';
const sheet =
spreadsheet.getSheetByName(sheetName) ||
spreadsheet.insertSheet(sheetName);
const subscriptions = getAllSubscriptions(site, apiKey);
const rows = [[
'Subscription ID',
'Customer ID',
'Customer Name',
'Subscription Value',
'Currency',
'Subscription Status',
'Payment Status',
'Latest Invoice ID',
'Updated At'
]];
subscriptions.forEach(function (item) {
const subscription = item.subscription || {};
const customer = item.customer || {};
const invoice = getLatestInvoice(
site,
apiKey,
subscription.id
);
const customerName = getCustomerName(customer);
const valueInMinorUnits = getSubscriptionValue(subscription);
const currencyCode =
subscription.currency_code ||
(invoice && invoice.currency_code) ||
'';
rows.push([
subscription.id || '',
subscription.customer_id || customer.id || '',
customerName,
convertMinorUnits(valueInMinorUnits, currencyCode),
currencyCode,
subscription.status || '',
invoice ? invoice.status || '' : 'NO_INVOICE',
invoice ? invoice.id || '' : '',
new Date()
]);
});
sheet.clearContents();
if (rows.length > 0) {
sheet
.getRange(1, 1, rows.length, rows[0].length)
.setValues(rows);
}
sheet.setFrozenRows(1);
sheet.autoResizeColumns(1, rows[0].length);
}
function getAllSubscriptions(site, apiKey) {
const results = [];
let offset = null;
do {
let path = '/api/v2/subscriptions?limit=100';
if (offset) {
path += '&offset=' + encodeURIComponent(offset);
}
const response = chargebeeGet(site, apiKey, path);
const list = response.list || [];
Array.prototype.push.apply(results, list);
offset = response.next_offset || null;
} while (offset);
return results;
}
function getLatestInvoice(site, apiKey, subscriptionId) {
if (!subscriptionId) {
return null;
}
const path =
'/api/v2/invoices' +
'?subscription_id%5Bis%5D=' + encodeURIComponent(subscriptionId) +
'&sort_by%5Bdesc%5D=date' +
'&limit=1';
const response = chargebeeGet(site, apiKey, path);
const list = response.list || [];
return list.length > 0 ? list[0].invoice || null : null;
}
function chargebeeGet(site, apiKey, path) {
const url = 'https://' + site + '.chargebee.com' + path;
const authorization = Utilities.base64Encode(apiKey + ':');
const response = UrlFetchApp.fetch(url, {
method: 'get',
headers: {
Authorization: 'Basic ' + authorization,
Accept: 'application/json'
},
muteHttpExceptions: true
});
const statusCode = response.getResponseCode();
const body = response.getContentText();
if (statusCode < 200 || statusCode >= 300) {
throw new Error(
'Chargebee API 请求失败。HTTP ' +
statusCode +
',响应内容:' +
body
);
}
return JSON.parse(body);
}
function getCustomerName(customer) {
const fullName = [
customer.first_name || '',
customer.last_name || ''
].join(' ').trim();
return fullName ||
customer.company ||
customer.email ||
customer.id ||
'';
}
function getSubscriptionValue(subscription) {
/*
* 这里将“订阅价值”定义为:
* 1. 优先使用 mrr;
* 2. 没有 mrr 时使用 plan_amount;
* 3. 再没有则使用 plan_unit_price × plan_quantity。
*
* 如果业务上的“订阅价值”是已付款金额,应改为读取 Invoice。
*/
if (subscription.mrr != null) {
return subscription.mrr;
}
if (subscription.plan_amount != null) {
return subscription.plan_amount;
}
if (subscription.plan_unit_price != null) {
return subscription.plan_unit_price *
(subscription.plan_quantity || 1);
}
return null;
}
function convertMinorUnits(amount, currencyCode) {
if (amount == null || amount === '') {
return '';
}
/*
* Chargebee 通常以货币最小单位返回金额。
* 例如 USD 1000 通常表示 10.00 USD。
* 不同货币的小数位数并不完全相同。
*/
const zeroDecimalCurrencies = [
'BIF', 'CLP', 'DJF', 'GNF', 'JPY',
'KMF', 'KRW', 'PYG', 'RWF', 'UGX',
'VND', 'VUV', 'XAF', 'XOF', 'XPF'
];
const threeDecimalCurrencies = [
'BHD', 'IQD', 'JOD', 'KWD', 'LYD',
'OMR', 'TND'
];
if (zeroDecimalCurrencies.indexOf(currencyCode) !== -1) {
return amount;
}
if (threeDecimalCurrencies.indexOf(currencyCode) !== -1) {
return amount / 1000;
}
return amount / 100;
}
首次运行 importChargebeeSubscriptions 时,Google 会请求访问 Spreadsheet 和外部网络的权限。完成授权后,脚本会创建或更新名为 Chargebee Subscriptions 的工作表。
API 调用的工作方式
Chargebee API 使用 HTTP Basic Authentication。API Key 作为用户名,密码留空:
Authorization: Basic base64(API_KEY:)
脚本先调用订阅列表接口:
GET https://{site}.chargebee.com/api/v2/subscriptions?limit=100
Chargebee 的列表接口会分页返回数据。如果响应中包含 next_offset,脚本会继续请求下一页,直至读取完所有订阅。
接着,脚本会为每个订阅查询最新发票:
GET /api/v2/invoices
?subscription_id[is]={subscription_id}
&sort_by[desc]=date
&limit=1
写入表格的主要字段来自以下数据:
| 表格字段 | 数据来源 |
|---|---|
| Customer Name | customer.first_name、customer.last_name 或 customer.company |
| Subscription Value | subscription.mrr、plan_amount 或计划单价与数量 |
| Subscription Status | subscription.status |
| Payment Status | 最新一张 invoice.status |
“订阅价值”需要先确定口径
“value of subscription”没有唯一的定义,常见口径有:
mrr:月度经常性收入,适合按月比较订阅价值plan_amount:当前计划金额- 最新发票的
total:最近一次实际开票金额 - 最新发票的
amount_paid:最近一次实际支付金额 - 多张发票的累计支付金额:客户生命周期内的已付收入
示例默认优先导出 subscription.mrr。如果需要最新发票金额,可以将循环中的金额处理改为:
const valueInMinorUnits = invoice ? invoice.total : null;
如果需要实际到账金额,可以使用:
const valueInMinorUnits = invoice ? invoice.amount_paid : null;
字段是否出现在响应中,取决于 Chargebee 站点配置、产品目录模式和返回对象。正式使用前,应检查一次实际的 API 响应。
订阅状态不等于付款状态
不能直接将 subscription.status 用作 payment status。例如,订阅可能仍然有效,但最新发票可能正处于待付款或未付款状态。
发票状态可以根据业务需要解释为:
paid:发票已支付payment_due:付款已到期或正在等待付款not_paid:未支付voided:发票已作废pending:发票仍在处理中NO_INVOICE:该订阅没有查询到发票
实际会出现哪些状态,应以当前 Chargebee API 的响应和所用 API 版本的文档为准。不要只根据这份列表写死所有业务规则。
数据量较大时的改进
示例会为每个订阅额外请求一次发票接口。假设有 1,000 个订阅,就可能产生约 1,001 次 API 请求,并可能超过 Google Apps Script 的单次运行时限。
数据量较大时,可以改为:
- 分页读取全部订阅;
- 分页读取所需时间范围内的发票;
- 在脚本中按
subscription_id建立索引; - 统一生成表格数据;
- 通过一次
setValues()批量写入。
也可以把分页游标或最后同步时间保存到 Script Properties,只同步新增或更新的数据,不必每次都全量覆盖。
设置自动更新
如需定时同步,可以在 Apps Script 中创建触发器:
- 打开左侧“触发器”
- 添加触发器
- 选择函数
importChargebeeSubscriptions - 将事件来源设为“时间驱动”
- 设置为每小时或每天运行
定时任务开始处理正式数据前,需要确认:
- API Key 权限是否仅限读取
- 表格中是否包含敏感的客户信息
- 共享权限是否合理
- 金额口径是否符合业务定义
- 发票状态是否符合财务团队的判断规则
- API 请求数量是否可能触及 Chargebee 或 Apps Script 的限制
问题中链接的 prod_cat_ver=1 表示文档按照特定产品目录模式展示,与示例请求路径中的 /api/v2/ 不是同一概念。如果站点使用其他产品目录模式,订阅中的计划、价格和订阅项字段可能不同,需要根据实际响应调整金额字段。
备注:内容仅供参考。
