Forked from dlh3/costco-receipts-extractor-snippet.js
Last active
January 30, 2026 23:43
-
-
Save mkobit/ca99b0caf8fe14955c65abdcb05383ce to your computer and use it in GitHub Desktop.
Costco JS console receipts extractor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * 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