# How to Integrate Replit with Etsy API

- Tool: Replit
- Difficulty: Intermediate
- Time required: 30 minutes
- Last updated: March 2026

## TL;DR

To integrate Replit with Etsy API, register an app on the Etsy developer portal, implement OAuth 2.0 PKCE flow from your Replit server, store credentials in Replit Secrets (lock icon 🔒), and call the Etsy Open API v3 endpoints for listings, shops, and receipts. Etsy uses OAuth 2.0 with PKCE (no client secret) for all operations that require seller authorization. Use an Autoscale deployment for the OAuth callback handler.

## Etsy API Integration from Replit: Listings, Shops, and Orders

Etsy's Open API v3 is the access point for the handmade marketplace's data: shop listings with photos and pricing, order receipts, shipping profiles, and seller analytics. For app builders, the Etsy API enables custom seller dashboards, inventory sync tools, order management automation, and listing analytics beyond what Etsy's native tools provide. With millions of active sellers, Etsy integrations are in demand for e-commerce tooling.

Etsy uses OAuth 2.0 with PKCE exclusively for API v3. Unlike older OAuth 2.0 implementations with client secrets, PKCE uses a cryptographically random code verifier generated per-request — there is no static secret to store or protect. Your Replit app generates a code verifier (random string), derives a code challenge from it (SHA256 hash), sends the challenge to Etsy's auth endpoint, and then proves ownership by sending the original verifier during the token exchange. This design is specifically intended for public clients and server-side apps that cannot safely store a secret.

Public data — listing search, shop info, listing photos — can be accessed with just your API key (x-api-key header) without OAuth. This is useful for marketplace search features or displaying Etsy shop data on a website. Seller-specific operations like reading private orders, updating listings, or accessing financial data all require a user OAuth token with appropriate scopes.

## Before you start

- A Replit account with a Node.js or Python Repl ready
- An Etsy seller account (required to access seller API features)
- An Etsy developer account at developers.etsy.com
- An Etsy app registered with OAuth redirect URI pointing to your Replit app URL

## Step-by-step guide

### 1. Register an Etsy Developer App

Navigate to developers.etsy.com and sign in with your Etsy account. If you do not have a developer account, you will be prompted to create one — this requires agreeing to the developer terms of service. Click 'Create a New App' and fill in the required fields: App Name (your integration's name), App Description (what the app does), and how you plan to use the API.

Under the OAuth configuration section, add a Redirect URI. This must be the URL of your Replit app's callback endpoint — for example, https://yourapp.yourusername.repl.co/auth/callback. For development, you can use your Replit dev URL. For production, use your deployed Autoscale URL or custom domain. Etsy only redirects to pre-registered redirect URIs, so you must update this when your URL changes.

After creating the app, you will see your Keystring (this is your API key / client ID). The Keystring is not secret — it identifies your app. You will include it in API requests as the x-api-key header for public endpoints. Etsy Open API v3 does NOT have a client secret — the PKCE flow is used instead, so you only have a Keystring.

Note your Keystring and the authorized scopes you will need. Common scopes are: listings_r (read listings), listings_w (write listings), transactions_r (read orders), shops_r (read shop data). Request only the scopes your app needs.

**Expected result:** An Etsy developer app exists with your Replit URL as the redirect URI. You have your app's Keystring (API key / client ID).

### 2. Store Etsy Credentials in Replit Secrets

Click the lock icon (🔒) in the left Replit sidebar to open the Secrets pane. Add the following secrets:

ETSY_CLIENT_ID: your app's Keystring (the API key from the developer portal — this identifies your app).

ETSY_REDIRECT_URI: the full redirect URI you registered (e.g., https://yourapp.yourusername.repl.co/auth/callback). This must match exactly what you registered on the developer portal.

ETSY_SCOPES: space-separated list of scopes your app needs (e.g., listings_r transactions_r shops_r).

When you complete the OAuth flow and receive tokens, also store them in Secrets:

ETSY_ACCESS_TOKEN: the OAuth access token received after PKCE authorization.

ETSY_REFRESH_TOKEN: the refresh token (if your app type supports offline access).

For a production multi-user app, you would store tokens in a database keyed by user ID rather than as environment variables. For a single-seller tool, storing the token in Secrets is fine.

The access token expires after 3600 seconds (1 hour). Your code must handle token expiry by catching 401 responses and refreshing the token automatically.

```
// check-etsy-secrets.js
const required = ['ETSY_CLIENT_ID', 'ETSY_REDIRECT_URI', 'ETSY_SCOPES'];
for (const key of required) {
  if (!process.env[key]) {
    throw new Error(`Missing secret: ${key}. Set it in Replit Secrets (lock icon 🔒).`);
  }
}
console.log('Etsy config OK.');
console.log('Client ID:', process.env.ETSY_CLIENT_ID);
console.log('Scopes:', process.env.ETSY_SCOPES);
```

**Expected result:** ETSY_CLIENT_ID, ETSY_REDIRECT_URI, and ETSY_SCOPES appear in Replit Secrets. The check script prints the client ID and scopes without errors.

### 3. Implement OAuth 2.0 PKCE Authorization Flow (Node.js)

Install dependencies in the Shell tab: npm install axios express crypto-js. The PKCE flow has three steps: generate a code verifier + challenge, redirect the user to Etsy's authorization page, and exchange the returned code for access tokens.

Step 1 — Generate PKCE values: create a random 43-128 character code verifier (alphanumeric + -._~), then generate a code challenge by taking the SHA256 hash of the verifier and base64url-encoding the result. Store the code verifier in the session (or a server-side cache) — you will need it in step 3.

Step 2 — Authorization redirect: build the Etsy authorization URL with query parameters: client_id (your Keystring), redirect_uri, response_type=code, scope (space-separated), state (random nonce for CSRF protection), and code_challenge + code_challenge_method=S256. Redirect the user to this URL.

Step 3 — Token exchange: when Etsy redirects to your callback with a code parameter, call Etsy's token endpoint (POST https://api.etsy.com/v3/public/oauth/token) with grant_type=authorization_code, client_id, redirect_uri, code, and code_verifier. Etsy returns an access_token and refresh_token. Store these securely.

For public data endpoints (no OAuth required), include x-api-key: {ETSY_CLIENT_ID} in request headers. For seller endpoints, include Authorization: Bearer {access_token} instead.

```
// etsy.js — Etsy OAuth 2.0 PKCE + API integration for Node.js on Replit
const express = require('express');
const axios = require('axios');
const crypto = require('crypto');

const app = express();
app.use(express.json());

const CLIENT_ID = process.env.ETSY_CLIENT_ID;
const REDIRECT_URI = process.env.ETSY_REDIRECT_URI;
const SCOPES = process.env.ETSY_SCOPES || 'listings_r transactions_r shops_r';

// In-memory store for PKCE verifiers (use Redis/DB in production)
const pkceStore = {};

function generateCodeVerifier() {
  return crypto.randomBytes(32).toString('base64url');
}

function generateCodeChallenge(verifier) {
  return crypto.createHash('sha256').update(verifier).digest('base64url');
}

// Step 1: Start OAuth — generate PKCE and redirect to Etsy
app.get('/auth/login', (req, res) => {
  const codeVerifier = generateCodeVerifier();
  const codeChallenge = generateCodeChallenge(codeVerifier);
  const state = crypto.randomBytes(16).toString('hex');
  
  // Store verifier for later (use session store in production)
  pkceStore[state] = codeVerifier;
  
  const params = new URLSearchParams({
    response_type: 'code',
    redirect_uri: REDIRECT_URI,
    scope: SCOPES,
    client_id: CLIENT_ID,
    state,
    code_challenge: codeChallenge,
    code_challenge_method: 'S256'
  });
  
  res.redirect(`https://www.etsy.com/oauth/connect?${params}`);
});

// Step 2: OAuth callback — exchange code for tokens
app.get('/auth/callback', async (req, res) => {
  const { code, state } = req.query;
  const codeVerifier = pkceStore[state];
  
  if (!codeVerifier) return res.status(400).send('Invalid state parameter');
  delete pkceStore[state];
  
  try {
    const tokenResponse = await axios.post('https://api.etsy.com/v3/public/oauth/token', {
      grant_type: 'authorization_code',
      client_id: CLIENT_ID,
      redirect_uri: REDIRECT_URI,
      code,
      code_verifier: codeVerifier
    });
    
    const { access_token, refresh_token, expires_in } = tokenResponse.data;
    // In production: store tokens securely in database
    // For single-user tool: update ETSY_ACCESS_TOKEN in Secrets manually
    console.log('OAuth complete. Access token expires in:', expires_in, 'seconds');
    res.json({ success: true, expires_in });
  } catch (err) {
    res.status(500).json({ error: err.response?.data || err.message });
  }
});

// Get current user's shop info (requires OAuth token)
app.get('/api/shop', async (req, res) => {
  const token = process.env.ETSY_ACCESS_TOKEN;
  try {
    const response = await axios.get('https://openapi.etsy.com/v3/application/users/me/shops', {
      headers: { 'Authorization': `Bearer ${token}`, 'x-api-key': CLIENT_ID }
    });
    res.json(response.data);
  } catch (err) {
    res.status(err.response?.status || 500).json({ error: err.response?.data || err.message });
  }
});

// Search public listings by keyword (API key only, no OAuth needed)
app.get('/api/listings/search', async (req, res) => {
  const { keywords, limit = 25 } = req.query;
  try {
    const response = await axios.get('https://openapi.etsy.com/v3/application/listings/active', {
      headers: { 'x-api-key': CLIENT_ID },
      params: { keywords, limit }
    });
    res.json(response.data);
  } catch (err) {
    res.status(err.response?.status || 500).json({ error: err.response?.data || err.message });
  }
});

app.listen(3000, '0.0.0.0', () => console.log('Etsy server running on port 3000'));
```

**Expected result:** Navigating to /auth/login redirects to Etsy's authorization page. After the seller authorizes, /auth/callback exchanges the code for tokens. GET /api/listings/search returns Etsy listings matching the keyword.

### 4. Python Integration for Etsy API

For Python Replit projects, install requests and flask: pip install requests flask. The PKCE flow works the same way as Node.js — generate the code verifier and challenge using Python's hashlib and base64 modules, redirect to Etsy's authorization URL, and exchange the code at the token endpoint.

Python's standard library has all the cryptographic primitives you need: secrets.token_urlsafe() for the code verifier, hashlib.sha256() for the challenge, and base64.urlsafe_b64encode() for base64url encoding. Note that base64url encoding must not have padding characters — use .rstrip('=') to remove them.

For Etsy API calls after OAuth, use requests.Session() with the Authorization header set. The access token expires in 3600 seconds. Implement a refresh token flow: when you receive a 401 response, call the token endpoint with grant_type=refresh_token, your CLIENT_ID, and the stored refresh_token to get a new access token.

For a single-seller tool deployed on Replit, run the OAuth flow once, copy the access_token and refresh_token from the logs or response, and store them in Replit Secrets. Add automatic refresh logic so the token renews without manual intervention.

```
# etsy_api.py — Etsy Open API v3 with OAuth 2.0 PKCE for Python on Replit
import os
import hashlib
import base64
import secrets
import requests
from flask import Flask, request, redirect, jsonify, session

app = Flask(__name__)
app.secret_key = secrets.token_hex(32)  # Set SECRET_KEY in Secrets for production

CLIENT_ID = os.environ['ETSY_CLIENT_ID']
REDIRECT_URI = os.environ['ETSY_REDIRECT_URI']
SCOPES = os.environ.get('ETSY_SCOPES', 'listings_r transactions_r shops_r')

def generate_pkce():
    """Generate PKCE code verifier and S256 challenge."""
    verifier = secrets.token_urlsafe(43)
    challenge = base64.urlsafe_b64encode(
        hashlib.sha256(verifier.encode()).digest()
    ).rstrip(b'=').decode()
    return verifier, challenge

@app.route('/auth/login')
def login():
    verifier, challenge = generate_pkce()
    state = secrets.token_hex(16)
    session['pkce_verifier'] = verifier
    session['oauth_state'] = state
    
    params = {
        'response_type': 'code',
        'redirect_uri': REDIRECT_URI,
        'scope': SCOPES,
        'client_id': CLIENT_ID,
        'state': state,
        'code_challenge': challenge,
        'code_challenge_method': 'S256'
    }
    auth_url = 'https://www.etsy.com/oauth/connect?' + '&'.join(f'{k}={v}' for k, v in params.items())
    return redirect(auth_url)

@app.route('/auth/callback')
def callback():
    code = request.args.get('code')
    state = request.args.get('state')
    
    if state != session.get('oauth_state'):
        return 'Invalid state', 400
    
    verifier = session.pop('pkce_verifier', None)
    if not verifier:
        return 'Missing PKCE verifier', 400
    
    response = requests.post('https://api.etsy.com/v3/public/oauth/token', json={
        'grant_type': 'authorization_code',
        'client_id': CLIENT_ID,
        'redirect_uri': REDIRECT_URI,
        'code': code,
        'code_verifier': verifier
    })
    
    if not response.ok:
        return jsonify({'error': response.json()}), response.status_code
    
    tokens = response.json()
    # Store tokens securely — update Secrets or save to database
    print('Access token received, expires in:', tokens['expires_in'], 'seconds')
    return jsonify({'success': True, 'expires_in': tokens['expires_in']})

@app.route('/api/receipts')
def get_receipts():
    """Get shop orders (requires OAuth token with transactions_r scope)."""
    token = os.environ.get('ETSY_ACCESS_TOKEN')
    if not token:
        return jsonify({'error': 'ETSY_ACCESS_TOKEN not set — run OAuth flow first'}), 401
    
    # First get shop ID
    me_resp = requests.get(
        'https://openapi.etsy.com/v3/application/users/me/shops',
        headers={'Authorization': f'Bearer {token}', 'x-api-key': CLIENT_ID}
    )
    if not me_resp.ok:
        return jsonify({'error': 'Failed to get shop info'}), me_resp.status_code
    
    shop_id = me_resp.json()['shop_id']
    receipts_resp = requests.get(
        f'https://openapi.etsy.com/v3/application/shops/{shop_id}/receipts',
        headers={'Authorization': f'Bearer {token}', 'x-api-key': CLIENT_ID},
        params={'limit': 25, 'was_paid': True}
    )
    return jsonify(receipts_resp.json())

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=3000)
```

**Expected result:** Visiting /auth/login redirects to Etsy's OAuth page. After authorization, /auth/callback exchanges the code for tokens. GET /api/receipts returns the shop's paid orders.

## Best practices

- Store ETSY_CLIENT_ID, ETSY_REDIRECT_URI, ETSY_SCOPES, and obtained tokens in Replit Secrets (lock icon 🔒) — never hardcode them
- Use server-side session storage (database or Redis) to persist PKCE verifiers across the OAuth redirect flow — in-memory storage is lost on server restart
- Request only the minimum Etsy scopes your app needs — this makes the OAuth consent screen less alarming to users and limits API exposure
- Implement automatic token refresh logic — Etsy access tokens expire after 1 hour; catch 401 responses and use the refresh_token to get a new access_token
- Validate the OAuth state parameter in your callback to prevent CSRF attacks — reject callbacks where the state does not match what you generated
- For public listing search that does not need seller data, use API key auth (x-api-key header) only — save OAuth flow for features that truly need seller account access
- Apply for production API access on the Etsy developer portal early — rate limits and feature availability differ between development and production apps
- Deploy with Autoscale on Replit for OAuth callback handlers and on-demand API endpoints; use Reserved VM only for continuous background sync jobs

## Use cases

### Seller Order Management Dashboard

Build a custom order management tool that pulls Etsy receipts (orders) into a unified dashboard alongside orders from other platforms. Track fulfillment status, customer information, and shipping details. Post shipping tracking numbers back to Etsy when orders ship.

Prompt example:

```
Build an Express app with Etsy OAuth PKCE login, a dashboard showing open orders with buyer details, and an endpoint to mark orders as shipped with a tracking number.
```

### Listing Inventory Sync

Sync Etsy listing quantities with an external inventory system. When stock is updated in your warehouse or spreadsheet, automatically update the corresponding Etsy listing quantity via the API. Prevent overselling on Etsy when items are sold on other platforms.

Prompt example:

```
Create an endpoint that accepts a listing ID and new quantity, updates the listing inventory quantity on Etsy via the API, and returns the updated listing details.
```

### Shop Analytics Aggregator

Pull Etsy shop stats — views, favorites, sales counts — and combine them with revenue data from order receipts to build custom analytics reports. Generate weekly summaries or trend charts that Etsy's native analytics does not provide.

Prompt example:

```
Build a Python script that fetches all Etsy receipts from the past 30 days, calculates total revenue by listing category, and outputs a summary report with top-selling items.
```

## Troubleshooting

### invalid_grant error during token exchange in the OAuth callback

Cause: The PKCE code verifier was lost between the login and callback requests (e.g., server restarted between steps, in-memory store cleared), or the authorization code has expired (codes expire after a few minutes), or the redirect_uri in the token request does not exactly match the one used in the authorization request.

Solution: Ensure the code verifier is persisted across the redirect (use a database-backed session rather than in-memory storage). Verify that REDIRECT_URI is identical in both the login redirect and the token exchange call — even a trailing slash difference will cause this error. Complete the OAuth flow quickly after starting it.

### 401 Unauthorized after OAuth completes — API calls fail

Cause: The access token has expired (Etsy tokens expire after 1 hour), the token was stored incorrectly in Secrets with extra whitespace, or the token was issued with insufficient scopes for the endpoint you are calling.

Solution: Check when the token was issued and whether 3600 seconds have passed. Implement a refresh flow using the refresh_token. Also verify the token's scopes match what the endpoint requires — for example, reading orders requires transactions_r scope.

```
// Refresh access token
async function refreshToken(refreshToken) {
  const response = await axios.post('https://api.etsy.com/v3/public/oauth/token', {
    grant_type: 'refresh_token',
    client_id: CLIENT_ID,
    refresh_token: refreshToken
  });
  return response.data.access_token;
}
```

### redirect_uri_mismatch error when starting OAuth flow

Cause: The ETSY_REDIRECT_URI in Replit Secrets does not exactly match the redirect URI registered in the Etsy developer portal. This includes differences in http vs https, trailing slashes, or path differences.

Solution: Log into the Etsy developer portal and copy the exact registered redirect URI. Update ETSY_REDIRECT_URI in Replit Secrets to match character-for-character. For Replit Autoscale deployments, the URL format is https://{repl-name}.{username}.repl.co/auth/callback.

```
// Debug redirect URI mismatch
console.log('Redirect URI:', JSON.stringify(process.env.ETSY_REDIRECT_URI));
```

### scope error — endpoint requires scope that was not requested

Cause: The OAuth flow was completed without requesting the necessary scope. For example, reading order receipts requires transactions_r but only listings_r was requested.

Solution: Update ETSY_SCOPES in Replit Secrets to include all required scopes (space-separated), then re-run the OAuth flow. The user must re-authorize with the new scopes. Etsy scope list: listings_r, listings_w, listings_d, transactions_r, transactions_w, billing_r, profile_r, profile_w, email_r, shops_rw, favorites_rw, feedback_r, recommended_listings_r.

## Frequently asked questions

### How do I connect Replit to Etsy API?

Register an app at developers.etsy.com to get your API Keystring, implement OAuth 2.0 PKCE in your Replit server to obtain seller access tokens, store the client ID and tokens in Replit Secrets (lock icon 🔒), and call the Etsy Open API v3 endpoints at openapi.etsy.com/v3/application/.

### Does Etsy API require a client secret?

No. Etsy Open API v3 uses OAuth 2.0 PKCE (Proof Key for Code Exchange), which does not use a client secret. Instead, your app generates a cryptographic code verifier per-request. You only need your app's Keystring (client ID), the OAuth code, and the code verifier to exchange for tokens — no stored client secret is involved.

### How do I store my Etsy API key in Replit?

Click the lock icon (🔒) in the Replit sidebar and add ETSY_CLIENT_ID (your app's Keystring), ETSY_REDIRECT_URI (your OAuth callback URL), and ETSY_SCOPES. After completing the OAuth flow, also store ETSY_ACCESS_TOKEN and ETSY_REFRESH_TOKEN in Secrets. Access them in code with process.env.ETSY_CLIENT_ID (Node.js) or os.environ['ETSY_CLIENT_ID'] (Python).

### Can I search Etsy listings without OAuth?

Yes. Public listing data — active listings, shop info, listing photos — can be accessed with just your API Keystring in the x-api-key header. OAuth is only required for seller-specific operations like reading orders, updating listings, or accessing financial data. For read-only marketplace search features, OAuth is not needed.

### Why do Etsy access tokens expire so quickly?

Etsy access tokens expire after 3600 seconds (1 hour) for security. Etsy provides refresh tokens that allow you to get a new access token without requiring the user to re-authorize. Implement a refresh flow that catches 401 responses and calls the token endpoint with grant_type=refresh_token. Store the latest access token in your database or update Replit Secrets.

---

Source: https://www.rapidevelopers.com/replit-integration/etsy-api
© RapidDev — https://www.rapidevelopers.com/replit-integration/etsy-api
