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.
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.
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
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() / 1000Pulling 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.