受到旁边楼启发,略微研究了一下amex的API后发现可以手动抓取所有amex自家卡(该API无法支持cobrand)的消费类别统计,包括bonus奖励(大部分来自开卡,副卡,refer)
最近发现通过奖励项目的变化时间能知道账号升降机准确生效日期
使用说明
以PC版chrome为基础
- 登录amex网页并切换至想要统计的卡,如果账号里只有一张卡则可跳过
- (再)点击一次切换卡的按钮,会弹出下拉框列表,卡多的账号还需要点击View All以显示全部卡。注意,此下拉框必须保持显示可见状态直至代码运行结束
- 按F12调出开发者工具,切换至console页面
- 粘贴代码并按回车
- 结果将在新的浏览器页面显示
备注1,统计结果默认为calendar year to date。举例如果改到2022,需要把首行定义为yr = 2022
Update 11/28/24
代码没有改动, 只是突然发现希尔顿和万豪的卡也能用这个api了
Update 9/27/24
新版本只适用于新amex界面,还是旧界面的用历史记录的版本
Update 4/5/24
更新匹配amex新的接口要求
Update 7/9/23
改进代码逻辑
Update 3/16/23
在报告页面增加显示使用统计数据的日期
Update 3/15/23
- 发现通过css-r4d5p3来获取token只适用于含有副卡的账户。改进后代码会先尝试使用该字段,若找不到则会重新读取完整页面来匹配token。
- 增加了fetch请求的次数,来应对某些时候无返回或超时的情况。
- 改进统计起始和结束日期的输入。
代码在此
yr = new Date().getFullYear();
queryStart = yr + "-01-01";
queryEnd = yr + "-12-31";
// read current active card token
cls = '[aria-controls="simple-switcher-listbox"]';
target = document.querySelector(cls);
img = target.getElementsByTagName('img')[0];
cardnum = target.querySelector('[data-testid="simple_switcher_display_number_val"]')
name = img.getAttribute('alt') + ' (' + cardnum.innerHTML + ')';
const run = +(target.getAttribute('aria-expanded')=='true');
if (run) {
token = target.getAttribute('aria-activedescendant');
token = token.slice(token.lastIndexOf('-')+1);
} else {
console.log('Please expand card switcher list before running');
}
//get card product data
for (var i = 0; i < 3*run; ++i) {
try {
cp = await fetch("https://functions.americanexpress.com/ReadLoyaltyBenefitsCardProduct.v1", {
"body": JSON.stringify({
"accountTokens":[token],
"cardNames":[],
"productType":"AEXP_CARD_ACCOUNT"}),
"method": "POST",
"mode": "cors",
"credentials": "include",
"headers": {
"accept": "application/json",
"content-type": "application/json",
"one-data-correlation-id": (Math.random() + 1).toString(36).substring(2)
},
}).then(res => res.json());
break;
} catch(err) {
setTimeout(() => { console.log(err.message); }, 5000);
}
}
try {
cardName = cp.cardDetails[0].cardName;
} catch(err) {
cardName = '';
}
// request reward summary for startDate to endDate, default ytd
for (var i = 0; i < 3*run; ++i) {
try {
res = await fetch("https://functions.americanexpress.com/ReadLoyaltyTransactionSummaries.v1", {
"body": JSON.stringify({
"accountToken":token,
"productType":"AEXP_CARD_ACCOUNT",
"startDate":queryStart,
"endDate":queryEnd,
"periodType":"CALENDAR_PERIOD",
"category":["REWARD"],
"summariesBy":["category","transactionType"],
"transactionType":["INDUSTRY_CATEGORY","BONUS","ADJUSTMENT"],
"includeSuppCards":true,
"cardProductName":cardName,
"summariesFor":"CARD_NUMBER"}),
"method": "POST",
"mode": "cors",
"credentials": "include",
"headers": {
"accept": "application/json",
"content-type": "application/json",
"one-data-correlation-id": (Math.random() + 1).toString(36).substring(2) }
}).then(res => res.json());
break;
} catch(err) {
setTimeout(() => { console.log(err.message); }, 5000);
}
}
// display in new tab
function display(res, img, name) {
if (!('summary' in res)) {
console.log(res.error);
console.log(name);
console.log(token);
return;
}
if (res.status['code'] != '0000') {
console.log(res.status);
console.log(name);
console.log(token);
return;
}
tab = window.open('about:blank', '_blank');
_table_ = tab.document.createElement('table'),
_tr_ = tab.document.createElement('tr'),
_th_ = tab.document.createElement('th'),
_td_ = tab.document.createElement('td');
_table_.style.border = '1px solid black';
_th_.style.border = '1px solid black';
_td_.style.border = '1px solid black';
// Builds the HTML Table out of myList json data from Ivy restful service.
function buildHtmlTable(arr) {
var table = _table_.cloneNode(false),
columns = addAllColumnHeaders(arr, table);
for (var i=0, maxi=arr.length; i < maxi; ++i) {
var tr = _tr_.cloneNode(false);
for (var j=0, maxj=columns.length; j < maxj ; ++j) {
var td = _td_.cloneNode(false);
cellValue = arr[i][columns[j]];
td.appendChild(document.createTextNode(arr[i][columns[j]] || ''));
tr.appendChild(td);
}
table.appendChild(tr);
}
return table;
}
// Adds a header row to the table and returns the set of columns.
// Need to do union of keys from all records as some records may not contain
// all records
function addAllColumnHeaders(arr, table)
{
var columnSet = [],
tr = _tr_.cloneNode(false);
for (var i=0, l=arr.length; i < l; i++) {
for (var key in arr[i]) {
if (arr[i].hasOwnProperty(key) && columnSet.indexOf(key)===-1) {
columnSet.push(key);
var th = _th_.cloneNode(false);
th.appendChild(document.createTextNode(key));
tr.appendChild(th);
}
}
}
table.appendChild(tr);
return columnSet;
}
data = JSON.parse(JSON.stringify(res.summary));
// modify data for better display
for (i = 0;i < data.length; i++) {
delete data[i]['benefitId'];
delete data[i]['summariesBy'];
delete data[i]['hasTracker'];
delete data[i]['totalAmount'];
if ('pointCount' in data[i]['summariesTotal'][0]) {
data[i]['Points'] = data[i]['summariesTotal'][0]['pointCount'];
}
if ('cash' in data[i]['summariesTotal'][0]) {
data[i]['Cash'] = data[i]['summariesTotal'][0]['cash']['amount'];
}
delete data[i]['summariesTotal'];
if ('tracker' in data[i]) {
data[i]['remainingAmount'] = data[i]['tracker']['remainingAmount']['value'];
}
delete data[i]['tracker'];
}
tab.document.body.append(name);
tab.document.body.appendChild(tab.document.createElement('br'));
tab.document.body.appendChild(tab.document.createElement('br'));
_img_ = tab.document.createElement('img');
_img_.setAttribute('class', img['alt']);
_img_.setAttribute('src', img['src']);
_img_.setAttribute('width', 300);
tab.document.body.appendChild(_img_);
tab.document.body.appendChild(tab.document.createElement('br'));
tab.document.body.appendChild(tab.document.createElement('br'));
tab.document.body.append('Summary from ' + res['period']['startDate'] + ' to ' + res['period']['endDate']);
tab.document.body.appendChild(buildHtmlTable(data));
tab.document.title = name;
}
if (run) display(res, img, name);