homedepot

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 23, 2026 License: MIT Imports: 13 Imported by: 0

README

homedepot-go

Go client for Home Depot's customer order-history API — the same internal endpoints the homedepot.com purchase-history page calls under the hood. Authentication is cookie replay from a logged-in browser session.

⚠️ Unofficial. Home Depot doesn't publish this API. Endpoint drift is possible and support is best-effort.

Install

go get github.com/fnziman/homedepot-go

Quick start

package main

import (
    "context"
    "fmt"
    "log/slog"
    "os"
    "time"

    "github.com/fnziman/homedepot-go"
)

func main() {
    logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
    client, err := homedepot.NewClient(homedepot.Config{
        Logger: logger,
        // CookieFile: "/custom/path/cookies.json", // optional; defaults to ~/.homedepot-api/cookies.json
    })
    if err != nil {
        panic(err)
    }

    ctx := context.Background()
    if err := client.HealthCheck(ctx); err != nil {
        panic(fmt.Errorf("auth: %w", err))
    }

    end := time.Now()
    start := end.AddDate(0, 0, -30)
    orders, err := client.ListOrders(ctx, start, end)
    if err != nil {
        panic(err)
    }
    for _, o := range orders {
        detail, err := client.GetOrder(ctx, o)
        if err != nil {
            fmt.Printf("skip %v: %v\n", o.OrderNumbers, err)
            continue
        }
        for _, item := range detail.AllLineItems() {
            fmt.Printf("  %s x%.0f  $%.2f\n", item.Description, item.PurchasedQuantity(), item.TotalPrice)
        }
    }
}

Exporting cookies

The client authenticates against the same session your browser holds. You need to export your homedepot.com cookies into a JSON file (default location: ~/.homedepot-api/cookies.json).

  1. Log in at homedepot.com.

  2. Open DevTools (Cmd+Option+I on Mac, F12 on Windows/Linux) and select the Console tab.

  3. Paste and run:

    copy(JSON.stringify(document.cookie.split('; ').map(c => {
      const i = c.indexOf('=');
      return { name: c.slice(0, i), value: c.slice(i + 1) };
    }), null, 2));
    console.log('Cookies copied to clipboard.');
    
  4. Save the clipboard contents to ~/.homedepot-api/cookies.json:

    mkdir -p ~/.homedepot-api
    pbpaste > ~/.homedepot-api/cookies.json   # macOS
    

    The file must contain a THD_CUSTOMER entry — that's the cookie the client decodes for auth.

Alternative: browser extension

If the DevTools snippet doesn't capture enough cookies (some anti-bot cookies are HTTP-only and won't appear in document.cookie), use an extension like EditThisCookie to export cookies for .homedepot.com and reshape into the same [{name, value}, ...] JSON format.

The client accepts either shape:

[{"name": "THD_CUSTOMER", "value": "..."}, ...]

or

{"cookies": [{"name": "THD_CUSTOMER", "value": "..."}, ...]}

What you can call

Method Purpose
NewClient(Config) Load cookies from Config.CookieFile (default ~/.homedepot-api/cookies.json) and construct a client.
NewClientWithCredentials(creds, cookies, Config) Advanced: build a client from already-loaded credentials + cookies (e.g. when they come from a secret manager or an env var).
client.HealthCheck(ctx) Confirms auth works. Small /orderhistory call.
client.ListOrders(ctx, start, end) All order summaries in the range. Walks year-by-year, paginates.
client.GetOrder(ctx, summary) Full detail (line items, tax, totals) for a summary. Routes to online or in-store based on summary.OrderOrigin.

Config fields (all optional): CookieFile string, Logger *slog.Logger, HTTPClient *http.Client, BaseURL string, Timezone string, PageSize int.

Errors and what they mean

Error Meaning Fix
ErrMissingCustomerCookie Cookie jar has no THD_CUSTOMER. You exported while logged out. Log in, re-export.
*AuthError (401 or plain 403) The session cookie expired, or an MFA step-up invalidated it. Re-export cookies from a fresh logged-in session.
ErrRateLimited (429, or 403 with rate limit in body) Home Depot throttled you. Back off and retry — the client already paces itself between pages.
*APIError (any other non-2xx) Server-side error or schema change. Inspect .Body; if the field set has moved, file an issue.

Known limitations

  • 24-month history cap — Home Depot's API only returns orders from roughly the last 24 months regardless of startDate. Older orders are not accessible via this API.
  • No programmatic login — homedepot.com uses Akamai anti-bot protection that blocks headless browsers. Cookie replay works; automated login does not. Every ~30 days (or on any MFA step-up) you'll need to re-export cookies.
  • Two response schemas — Online orders (orderOrigin: "online") and in-store purchases (orderOrigin: "instore") return the same top-level shape but with different key populations. GetOrder handles the routing; OrderDetail.AllLineItems() flattens both.
  • orginalOrderedQuantity typo — the upstream JSON key really is spelled that way (missing the "i"). We expose it as LineItem.OriginalOrderedQty.

Development

go test ./... -race -cover
golangci-lint run ./...

Test fixtures under testdata/ are hand-crafted — no real cookies, tokens, addresses, emails, phone numbers, payment info, or personal purchase history. If you contribute new fixtures, scrub the same way.

Acknowledgements

The API schema, auth flow, and endpoint mapping were reverse-engineered by joshellissh/homedepot-history (MIT). This library ports that work to Go with an idiomatic client surface.

License

MIT — see LICENSE.

Documentation

Overview

Package homedepot is a Go client for Home Depot's internal customer order history API (the same endpoints the homedepot.com purchase-history page itself calls). Authentication is cookie replay: the caller exports their browser cookies to a JSON file after logging in, and the client decodes the THD_CUSTOMER cookie for the auth token.

The API is unofficial and endpoint drift is possible. Callers should treat 4xx responses as either auth expiration (AuthError) or schema changes.

This client is inspired by the schema mapping in https://github.com/joshellissh/homedepot-history (MIT).

Index

Constants

View Source
const (
	DefaultBaseURL  = "https://www.homedepot.com"
	DefaultTimezone = "America/New_York"
)

Configurable defaults.

Variables

View Source
var ErrEmptyCookieFile = errors.New("cookie file is empty")

ErrEmptyCookieFile is returned when the cookies file parses but has no entries.

View Source
var ErrMissingCustomerCookie = errors.New("THD_CUSTOMER cookie not found in jar; export cookies while logged in to homedepot.com")

ErrMissingCustomerCookie is returned when the cookie jar does not contain a THD_CUSTOMER cookie — usually because the user exported cookies while logged out.

View Source
var ErrRateLimited = errors.New("home depot API rate-limited the request")

ErrRateLimited is returned when Home Depot's API rejects a request as rate-limited. Callers can back off and retry.

Functions

func DefaultCookiePath

func DefaultCookiePath() (string, error)

DefaultCookiePath returns the canonical on-disk location for the exported cookies file: $HOME/.homedepot-api/cookies.json.

func ExtractCustomerCookie

func ExtractCustomerCookie(cookies []Cookie) (string, error)

ExtractCustomerCookie returns the value of the THD_CUSTOMER cookie from a jar, or ErrMissingCustomerCookie if it is not present.

func LoadFromFile

func LoadFromFile(path string) (Credentials, []Cookie, error)

LoadFromFile is the convenience one-shot: reads the cookie jar at path, finds THD_CUSTOMER, decodes it, and returns everything the client needs.

Types

type APIError

type APIError struct {
	StatusCode int
	Body       string
}

APIError is a non-auth HTTP error from the Home Depot API.

func (*APIError) Error

func (e *APIError) Error() string

type AuthError

type AuthError struct {
	StatusCode int
	Body       string
}

AuthError signals the request was rejected as unauthenticated (typically 401 or 403). Usually means the exported cookies have expired or an MFA step-up has invalidated the session — the user needs to log in again and re-export cookies.

func (*AuthError) Error

func (e *AuthError) Error() string

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is the Home Depot API client.

func NewClient

func NewClient(cfg Config) (*Client, error)

NewClient reads cookies from cfg.CookieFile (defaulting to DefaultCookiePath() when empty), decodes the THD_CUSTOMER cookie for auth, and returns a ready-to-use Client. This is the canonical constructor.

func NewClientWithCredentials

func NewClientWithCredentials(creds Credentials, cookies []Cookie, cfg Config) *Client

NewClientWithCredentials builds a Client from already-loaded credentials + cookies. Use this when cookies come from somewhere other than a JSON file on disk (a secret manager, an env var, a unit test, etc.). cfg.CookieFile is ignored on this path.

func (*Client) GetOrder

func (c *Client) GetOrder(ctx context.Context, summary OrderSummary) (OrderDetail, error)

GetOrder fetches full detail for a single order summary. Routes to the online or in-store /orderdetails variant based on summary.OrderOrigin.

func (*Client) HealthCheck

func (c *Client) HealthCheck(ctx context.Context) error

HealthCheck issues a small /orderhistory call to verify auth works. It returns nil on success and an AuthError, ErrRateLimited, or APIError on the various failure modes.

func (*Client) ListOrders

func (c *Client) ListOrders(ctx context.Context, start, end time.Time) ([]OrderSummary, error)

ListOrders returns every order summary whose salesDate falls in the [start, end] range (inclusive). Walks year-by-year, mirroring the reference impl, with polite pauses between pages.

Note: Home Depot's API only returns orders from approximately the last 24 months, so requests older than that will yield nothing regardless.

func (*Client) UserID

func (c *Client) UserID() string

UserID returns the userId extracted from the THD_CUSTOMER cookie.

type Config

type Config struct {
	// CookieFile is the path to a JSON cookie jar exported from a logged-in
	// browser session. Only consulted by NewClient. Empty means use the
	// canonical location (DefaultCookiePath()).
	CookieFile string

	// Logger receives structured client-side diagnostics. Nil means a no-op
	// logger — callers who want visibility should always pass a real
	// *slog.Logger (typically the caller's own scoped provider logger).
	Logger *slog.Logger

	// HTTPClient overrides the default outbound HTTP client. Useful for
	// injecting a transport with rate limiting or tracing. Nil means the
	// default (30s timeout).
	HTTPClient *http.Client

	// BaseURL overrides the API host. Empty means DefaultBaseURL. Primarily
	// used by tests pointing at an httptest.Server.
	BaseURL string

	// Timezone is the IANA timezone string sent on every request. Empty means
	// DefaultTimezone.
	Timezone string

	// PageSize is the /orderhistory page size. Zero or negative means 100.
	PageSize int
}

Config is the constructor parameter for NewClient / NewClientWithCredentials. Every field is optional; sensible defaults are applied for empty values.

Design intent: matches the ClientConfig / Config pattern used by the sibling clients walmart-client-go and costco-go so callers (itemize) can wire all three the same way.

type Cookie struct {
	Name   string `json:"name"`
	Value  string `json:"value"`
	Domain string `json:"domain,omitempty"`
	Path   string `json:"path,omitempty"`
}

Cookie is a single browser cookie the client will send on every request. Only Name and Value are strictly needed; Domain and Path are preserved for documentation but not used when serializing the outbound request.

func LoadCookies

func LoadCookies(path string) ([]Cookie, error)

LoadCookies reads and parses a cookie file from disk. Two JSON shapes are accepted so the file works with both bare-array exports and explicitly wrapped ones:

[{"name": "...", "value": "..."}, ...]
{"cookies": [{"name": "...", "value": "..."}, ...]}

type Credentials

type Credentials struct {
	UserID            string
	AuthToken         string
	CustomerAccountID string
}

Credentials is the identity + auth token extracted from the THD_CUSTOMER cookie. UserID is used to build the request URL; AuthToken is sent in the Authorization header on every request; CustomerAccountID is required for in-store order-details lookups.

func DecodeCustomerCookie

func DecodeCustomerCookie(value string) (Credentials, error)

DecodeCustomerCookie decodes a THD_CUSTOMER cookie value into Credentials.

The cookie value is a dot-separated triple; the first segment is base64-encoded JSON of the form {"u": userId, "i": authToken, "t": customerAccountId}. Whitespace inside u and t is stripped (matches the reference implementation's behavior).

type FulfillmentGroup

type FulfillmentGroup struct {
	LineItems []LineItem `json:"lineItems"`
}

FulfillmentGroup groups line items by fulfillment method (ship-to-home, BOPIS, etc.). We only care about the line items for categorization.

type LineItem

type LineItem struct {
	THDSKU             string  `json:"thdSku"`
	LineID             string  `json:"lineId"`
	SKUNumber          string  `json:"skuNumber"`
	ModelNumber        string  `json:"modelNumber"`
	BrandName          string  `json:"brandName"`
	Description        string  `json:"description"`
	UnitPrice          float64 `json:"unitPrice"`
	TotalPrice         float64 `json:"totalPrice"`
	ShippingCharge     float64 `json:"shippingCharge"`
	CurrentQuantity    float64 `json:"currentQuantity"`
	OriginalOrderedQty float64 `json:"orginalOrderedQuantity"` // sic — upstream typo
	CancelledQuantity  float64 `json:"cancelledQuantity"`
	IsGiftCard         bool    `json:"isGiftCard"`
	ImageURL           string  `json:"imageUrl"`
	UPCCode            string  `json:"upcCode"`
	StatusDescription  string  `json:"statusDescription"`
}

LineItem is a single purchased item.

Notable API quirk: OriginalOrderedQty maps to the API field "orginalOrderedQuantity" — that spelling with the missing "i" is the upstream typo, not ours.

func (LineItem) PurchasedQuantity

func (i LineItem) PurchasedQuantity() float64

PurchasedQuantity is CurrentQuantity minus CancelledQuantity, clamped at zero. Use this in preference to CurrentQuantity when computing what the customer actually paid for.

type OrderDetail

type OrderDetail struct {
	OrderNumber       string             `json:"orderNumber"`
	CustomerAccountID string             `json:"customerAccountId"`
	UserID            string             `json:"userId"`
	OrderOrigin       string             `json:"orderOrigin"`
	StatusDescription string             `json:"statusDescription"`
	SalesDate         string             `json:"salesDate"`
	SubTotalAmount    float64            `json:"subTotalAmount"`
	TaxTotalAmount    float64            `json:"taxTotalAmount"`
	GrandTotalAmount  float64            `json:"grandTotalAmount"`
	ShippingCharge    float64            `json:"shippingCharge"`
	DeliveryCharge    float64            `json:"deliveryCharge"`
	StoreNumber       string             `json:"storeNumber"`
	StoreName         string             `json:"storeName"`
	FulfillmentGroups []FulfillmentGroup `json:"fulfillmentGroups"`
}

OrderDetail is the response from /orderdetails for a single order. The commented-out fields are present in real responses but unused by itemize (kept in the JSON so nothing breaks if we start using them later).

func (OrderDetail) AllLineItems

func (o OrderDetail) AllLineItems() []LineItem

AllLineItems flattens line items across every fulfillment group.

type OrderSummary

type OrderSummary struct {
	OrderOrigin    string   `json:"orderOrigin"` // "online" or "instore"
	OrderNumbers   []string `json:"orderNumbers"`
	SalesDate      string   `json:"salesDate"`
	StoreNumber    string   `json:"storeNumber"`
	StoreName      string   `json:"storeName"`
	TotalAmount    float64  `json:"totalAmount"`
	OrderStatus    string   `json:"orderStatus"`
	RegisterNumber string   `json:"registerNumber"` // in-store only
	TransactionID  string   `json:"transactionId"`  // in-store only
}

OrderSummary is one entry from the /orderhistory response. The field set is deliberately small — this is only what's needed to (a) render a listing and (b) request full details later.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL