Etsy & Oauth2

December 5, 2025 Ā· Tyler Yeager Ā· 5 min reading time

Etsy provides an API that is free to use (at least for Personal Access)S If you think you might want to use their API to build against, get a website (an Etsy shop page will do) and register for one now. It can take weeks to get approved or denied. So start early.

Simple Request

Some of the requests are easier to parse against. For example, you can get featured listings by shop with just the api key they give you.

main.ts
const shopId='1234'
const apiKey='8217f64e-9180-4abc-a26b-10f872bc8316'
const res = await fetch(`https://openapi.etsy.com/v3/application/shops/${shopId}/listings/featured`, {
method: "GET",
headers: {
Accept: "application/json",
Authentication: `Bearer ${apiKey}`
}
})
if(res.ok) {
const resJson = await res.json();
console.log(resJson)
}

There’s some useful information here, and I encourage you to explore around. Everywhere you have ā€œAUTHORIZATIONSā€ state api_key you’re good. You can just use your token and keep things simple.

But there’s a very useful bit of information that every shop owner wants to know, and it’s not accessible with just the api_key…

Get Shop Receipts c Make

When you get an order, I bet you want to know about it. Who is it, what did they buy, what personalization information did they put in? What’s their address?

Well, you’re out of luck on that last one. But the rest of that information is super valuable for even the smallest of automations.

You might notice on this page for getShopReceipts that the AUTHORIZATIONS now says api_key AND oauth2 (transactions_r)

Guess what? Now you have to manage oauth2 to be able to access this.

One strategy that I took advantage of for over a year is using a SAAS to handle the complexity. I wrote a function through make.com that essentially did the following:

  • Poll Etsy
  • For each new order, parse the request into JSON
  • POST the request to my webhook

I didn’t like this, because make.com chews up a lot of tokens doing this basic task. Checking takes a token, parsing into json takes a token, making a network request takes a token. Oh, the free plan is 1000 tokens a month.

So if you checked once per hour, that’s 24 / day or 720 a month. This is with getting 0 orders.

So, how can we do this better?

Get Shop Receipts with Windmill

Well, by managing the oauth refresh & access token ourselves and cutting out the middleman!

You can apply this to any serverless function (AWS Lambda & Friends), or a server that you write. At the core, here is what you need to do

  • Get an initial Etsy oauth refresh & access token.
  • When making a request, check to see if it’s expired, and renew if needed
  • Make our request with our valid token

Getting the initial token

For this, I used insomnia. They conveniently offer the ability to debug doing authentication with oauth2 and crucially, show the refresh & access token. I’ve included a picture of the settings that I used. Once you have the tokens, you won’t need the app anymore. It’s one-time (until something breaks)

Check, Renew & Request

The tricky part here is setting and getting state. A lot of platforms make it easy to get state. Think .env, configuration files or getting variables from databases. But we need to set state too. How you do it is up to you. You have to have it. For me, I used Windmill, so my critical function was setResource I loaded my resource (a json blob), modified it and set it when done.

What did I modify?

  • The access token
  • The refresh token
  • Token expiration time
  • Time last run

I’m going to include code here, it may not apply to you. Try to use it as a general idea of how to implement it. But let’s walk through it. Firstly, r shows we will be returning a string. This will be the access token. You can use this token to make all of those privileged api_key & oauth2 requests.

We start by getting the resource. Note that a resource is just a json blob. We also modify and set it later in the function s .

The first thing we check to see is if it’s expired. If now is greater than the timestamp, it’s expired. So it’s time to get a new one. They last about 1 hour, so expect it to be exchanged a lot.

main.ts
import * as wmill from "windmill-client";
const etsyApiPrefix = "https://api.etsy.com/v3";
export async function refreshAccessToken(refreshToken?: string): Promise<string> {
const resource = (await wmill.getResource('f/etsyResource'))
const oauth = resource.oauth;
const now = new Date;
// Check to see if it's expired. Compare to now versus the expiration time.
if (now.getTime() > new Date(resource.expirationTime).getTime()) {
const refreshTokenRes = await fetch(`${etsyApiPrefix}/public/oauth/token`, {
method: 'POST',
body: new URLSearchParams({
grant_type: 'refresh_token',
client_id: resource.clientId,
refresh_token: refreshToken ? refreshToken : resource.oauth.refreshToken,
})
})
if(refreshTokenRes.ok) {
console.log("Updating token")
const refreshTokenJson = await refreshTokenRes.json() as TokenRefreshResponse;
oauth.accesToken = refreshTokenJson.access_token;
oauth.refresToken = refreshTokenJson.refresh_token;
oauth.expiration_time = now.getTime() + refreshTokenJson.expires_in * 1000;
resource.oauth = oauth;
await wmill.setResource(resource, 'f/etsyResource');
} else {
console.log(refreshTokenRes.statusText)
}
}
return oauth.access_token
}
type TokenRefreshResponse = {
access_token: string,
token_type: string,
expires_in: number,
refresh_token: string,
user_id: null,
api_key: null,
}

With this function, we can reliably make requests. We know if we make a request, it will either use the existing access token, or get a new one for us to use.

Grabbing Receipts

We’re going to use the refreshAccessToken r and finally make our request. Here we’re using axios to simplify much of the boilerplate for our Etsy API requests.

We also use URLSearchParams u to simplify the query params.

If you have a lot of sales, this will return all of them, we’ll filter this in a moment

request.ts
import axios from "axios";
const shopId='1234'
const apiKey='8217f64e-9180-4abc-a26b-10f872bc8316'
const accessToken = await refreshAccessToken();
const etsyApi = axios.create({
baseURL: "https://api.etsy.com/v3",
timeout: 5000,
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"X-API-Key": apiKey,
"Authorization": `Bearer ${accessToken}`,
},
})
const shopReceiptParams = new URLSearchParams({
client_id: clientId,
scope: 'transactions_r',
state: "some-random-value",
})
const res = await etsyApi.get(`/application/shops/${shopId}/receipts?${shopReceiptParams}`)

Filtering To Unread Results

I expect one of the most common needs is to reliably get new receipts and insert them into another database, or do some other kind of action on then. For example, validate all orders emails are legitimate.

To this end, Etsy offers the following query param: min_created. That is, only includes results that have been created after this timestamp.

But, what is ā€œthisā€ timestamp? It should be the last time you ran this function! So, at this point, there’s 3 things we need to track

  • Refresh token
  • Access token
  • Last run timestamp

Decide how you’re going to store the last run variable. Personally, I reused the json object for refresh & access tokens, so it looked something like this:

{
"oauth": {
"access_token": "abc1234",
"refresh_token": "def5678"
},
"last_run": 1765313596793,
}

Keep in mind your languages timestamp quirks. For example, in javascript, they measure in milliseconds, while other timestamps record in seconds. I believe this query uses seconds. So if you want to use javascript, you’ll need to divide by 1000.

const lastRun = (new Date()).getTime() / 1000

Pulling It Together

Alright! Assuming you figured out a way to store state, and the fact that we have a function that will handling oauth refreshes for us, and we know how to do the query in general, we can incorporate this to make a useful function.

import axios from "axios";
const shopId='1234'
const apiKey='8217f64e-9180-4abc-a26b-10f872bc8316'
const accessToken = await refreshAccessToken();
const etsyApi = axios.create({
baseURL: "https://api.etsy.com/v3",
timeout: 5000,
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"X-API-Key": apiKey,
"Authorization": `Bearer ${accessToken}`,
},
})
const shopReceiptParams = new URLSearchParams({
client_id: clientId,
scope: 'transactions_r',
state: "some-random-value",
min_value: resource.last_run
})
await wmill.setResource(resource);
const res = await etsyApi.get(`/application/shops/${shopId}/receipts?${shopReceiptParams}`)

You can add in your own logic to use the res.data Etsy response, but it is ready to be run on schedule. Personally, I loop over each new order and add it to my own database and validate the email. There’s more you can do, potentially validating variation information.

Did you find this interesting?

Consider subscribing 😊

No AI-generated content or SEO garbage.

Unsubscribe anytime