Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save mkobit/ca99b0caf8fe14955c65abdcb05383ce to your computer and use it in GitHub Desktop.

Select an option

Save mkobit/ca99b0caf8fe14955c65abdcb05383ce to your computer and use it in GitHub Desktop.
Costco JS console receipts extractor
/**
* Costco web receipt exporter
*
* Run in browser console at https://www.costco.com/OrderStatusCmd
* Requires Temporal API (Chrome 118+)
*
* Usage:
* // Last N months
* const lastMonth = (n = 1) => {
* const end = Temporal.Now.plainDateISO();
* const start = end.subtract({ months: n });
* return main(start, end);
* };
* await lastMonth(); // last 1 month
* await lastMonth(3); // last 3 months
*
* // Full year
* await main(Temporal.PlainDate.from('2024-01-01'), Temporal.PlainDate.from('2024-12-31'));
* await main(Temporal.PlainDate.from('2025-01-01'), Temporal.PlainDate.from('2025-12-31'));
*/
/** @type {{url: string, clientIdentifier: string, downloadDelay: Temporal.Duration}} */
const API_CONFIG = Object.freeze({
url: 'https://ecom-api.costco.com/ebusiness/order/v1/orders/graphql',
clientIdentifier: '481b1aec-aa3b-454b-b81b-48187e28f205',
downloadDelay: Temporal.Duration.from({ milliseconds: 150 }),
});
//////////////////////
// GRAPHQL QUERIES
//////////////////////
/** @type {string} */
const RECEIPTS_QUERY = `
query receiptsWithCounts($startDate: String!, $endDate: String!, $documentType: String!, $documentSubType: String!) {
receiptsWithCounts(startDate: $startDate, endDate: $endDate, documentType: $documentType, documentSubType: $documentSubType) {
inWarehouse
gasStation
carWash
gasAndCarWash
receipts {
warehouseName
receiptType
documentType
transactionDateTime
transactionDate
companyNumber
warehouseNumber
operatorNumber
warehouseShortName
registerNumber
transactionNumber
transactionType
transactionBarcode
warehouseAddress1
warehouseAddress2
warehouseCity
warehouseState
warehouseCountry
warehousePostalCode
totalItemCount
subTotal
taxes
total
instantSavings
membershipNumber
itemArray {
itemNumber
itemDescription01
itemDescription02
frenchItemDescription1
frenchItemDescription2
itemIdentifier
itemDepartmentNumber
unit
amount
taxFlag
merchantID
entryMethod
transDepartmentNumber
fuelUnitQuantity
fuelGradeCode
itemUnitPriceAmount
fuelUomCode
fuelUomDescription
fuelGradeDescription
}
tenderArray {
tenderTypeCode
tenderSubTypeCode
tenderDescription
amountTender
displayAccountNumber
sequenceNumber
approvalNumber
responseCode
tenderTypeName
transactionID
merchantID
entryMethod
tenderAcctTxnNumber
tenderAuthorizationCode
tenderTypeNameFr
tenderEntryMethodDescription
walletType
walletId
storedValueBucket
}
couponArray {
upcnumberCoupon
voidflagCoupon
refundflagCoupon
taxflagCoupon
amountCoupon
}
subTaxes {
tax1
tax2
tax3
tax4
aTaxPercent
aTaxLegend
aTaxAmount
aTaxPrintCode
aTaxPrintCodeFR
aTaxIdentifierCode
bTaxPercent
bTaxLegend
bTaxAmount
bTaxPrintCode
bTaxPrintCodeFR
bTaxIdentifierCode
cTaxPercent
cTaxLegend
cTaxAmount
cTaxIdentifierCode
dTaxPercent
dTaxLegend
dTaxAmount
dTaxPrintCode
dTaxPrintCodeFR
dTaxIdentifierCode
uTaxLegend
uTaxAmount
uTaxableAmount
}
}
}
}
`;
//////////////////////
// API CLIENT
//////////////////////
const CostcoApi = Object.freeze({
/** @returns {Record<string, string>} */
getHeaders: () => ({
'Content-Type': 'application/json-patch+json',
'Costco.Env': 'ecom',
'Costco.Service': 'restOrders',
'Costco-X-Wcs-Clientid': localStorage.getItem('clientID'),
'Client-Identifier': API_CONFIG.clientIdentifier,
'Costco-X-Authorization': `Bearer ${localStorage.getItem('idToken')}`,
}),
/**
* @param {Temporal.PlainDate} startDate
* @param {Temporal.PlainDate} endDate
* @returns {Promise<{counts: {inWarehouse: number, gasStation: number, carWash: number}, receipts: Receipt[]}>}
*/
fetchReceipts: (startDate, endDate) =>
fetch(API_CONFIG.url, {
method: 'POST',
headers: CostcoApi.getHeaders(),
body: JSON.stringify({
query: RECEIPTS_QUERY.replace(/\s+/g, ' '),
variables: {
startDate: formatDate(startDate),
endDate: formatDate(endDate),
documentType: 'all',
documentSubType: 'all',
},
}),
})
.then((r) => r.ok ? r.json() : Promise.reject(new Error(`API error: ${r.status}`)))
.then((json) => {
if (json.errors) throw new Error(`GraphQL: ${JSON.stringify(json.errors)}`);
const data = json.data.receiptsWithCounts;
return {
counts: { inWarehouse: data.inWarehouse, gasStation: data.gasStation, carWash: data.carWash },
receipts: data.receipts,
};
}),
});
//////////////////////
// DATE FORMATTING
//////////////////////
/**
* @param {Temporal.PlainDate} date
* @returns {string} MM/DD/YYYY
*/
const formatDate = (date) =>
date.toLocaleString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric' });
//////////////////////
// EXPORT
//////////////////////
/**
* @param {Temporal.Duration} duration
* @returns {Promise<void>}
*/
const delay = (duration) => new Promise((resolve) => setTimeout(resolve, duration.total('milliseconds')));
/**
* @param {object} data
* @param {string} filename
*/
const downloadJson = (data, filename) => {
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
/**
* @param {Receipt} receipt
* @returns {string}
*/
const buildReceiptFilename = (receipt) => {
const type = receipt.documentType === 'FuelReceipts' ? 'gas' : 'warehouse';
return `${receipt.transactionDate}_costco_${type}_${receipt.transactionBarcode}.json`;
};
/**
* @param {Receipt[]} receipts
* @param {Temporal.Duration} delayDuration
*/
const downloadAllReceipts = async (receipts, delayDuration = API_CONFIG.downloadDelay) => {
console.log(`Downloading ${receipts.length} receipts (${delayDuration.total('milliseconds')}ms delay between files)...`);
for (const [index, receipt] of receipts.entries()) {
const filename = buildReceiptFilename(receipt);
downloadJson(receipt, filename);
console.log(`[${index + 1}/${receipts.length}] ${filename}`);
if (index < receipts.length - 1) {
await delay(delayDuration);
}
}
console.log('Done.');
};
//////////////////////
// MAIN
//////////////////////
/**
* @param {Temporal.PlainDate} startDate
* @param {Temporal.PlainDate} endDate
* @returns {Promise<{receipts: Receipt[], counts: object, membershipNumber: string|null}>}
*/
const main = async (startDate, endDate) => {
console.log(`Fetching Costco receipts: ${startDate} to ${endDate}`);
const { counts, receipts } = await CostcoApi.fetchReceipts(startDate, endDate);
console.log(`Fetched ${receipts.length} receipts (warehouse: ${counts.inWarehouse}, gas: ${counts.gasStation}, carWash: ${counts.carWash})`);
if (receipts.length === 0) {
console.warn('No receipts found for date range');
return { receipts: [], counts, membershipNumber: null };
}
const membershipNumber = receipts[0].membershipNumber;
await downloadAllReceipts(receipts);
return { receipts, counts, membershipNumber };
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment