Reverse-Engineering an API

Lesson 11 — Reverse-Engineering an API | Firefox DevTools
Lesson 11 of 11 · Scenario ✓ Final Lesson

Reverse-Engineering an API

Firefox Developer Tools · Network Panel · Console · Debugger · cURL · fetch()
Every web application talks to an API. Even when there is no public documentation, DevTools lets you observe every request the browser makes, read the full payload, and replay it from a script, terminal, or Postman. This is one of the most practical skills you can develop as a developer.
⚠ Ethics and legality Reverse-engineering an API for your own application, integration work, or debugging is normal engineering. Automating requests against a service you don't own, bypassing rate limits, or violating a service's Terms of Service may have legal consequences under laws such as the CFAA (US), Computer Misuse Act (UK), or GDPR (EU). These techniques are neutral tools — use them responsibly.

The Workflow at a Glance

1
Set up the Network panel
Disable cache, enable Persist Logs, filter to XHR/Fetch, clear existing requests.
2
Trigger actions and observe requests
Click through the UI. Each action fires API calls. Use the URL filter, Ctrl+F response search, and timing sort to find the relevant request.
3
Read and document the request
Headers tab: URL, method, auth. Request tab: body for POST/PUT. Response tab: data shape, pagination, field names.
4
Replay the request
Copy as cURL, replay from the Console with fetch(), or use Edit and Resend to modify parameters without writing code.
5
Identify patterns across requests
URL naming, pagination style, authentication pattern, rate limit headers, hidden query parameters.
6
Find source clues in the Debugger
Use the Initiator column to jump to the fetch() call site. Read the source to discover all possible parameters.

Step 1 — Set Up the Network Panel

Network All XHR/Fetch ✓ JS CSS ☑ Disable Cache ☑ Persist Logs 🗑 Clear
Status
Method
URL
Size
Time
Perform an action in the application to capture requests…

Four settings matter before you start: Disable Cache (forces fresh requests), Persist Logs (survives page navigations), XHR/Fetch filter (removes HTML/CSS/image noise), and Clear the log so you start from a clean slate.

Step 2 — Trigger Actions and Observe

Click through the application performing the action you want to understand. Then find the relevant request using these techniques:

TechniqueHowBest for
URL text filterType users or api in the filter boxWhen you know part of the endpoint path
Timing sortClick Waterfall column → sort by start timeFinding the request that fired right after your click
Response body searchCtrl + F → type a value from the UIFinding which endpoint returned a specific username, price, or ID
Method filterClick the method column to sort; use filter box POSTIsolating write operations from reads
Network XHR/Fetch 🔍 products
Status
Method
URL
Size
Time
200
GET
api.example.com/v2/products?category=electronics&page=1&limit=20
8.4 KB
210ms
200
POST
api.example.com/v2/products
0.9 KB
340ms

Step 3 — Read and Document the Request

Anatomy of the URL

https://api.example.com/v2/products?category=electronics&page=1&limit=20
Base URL
Version prefix
Resource path
Query parameters

Request detail tabs

Headers
Request
Response
Timings
Request Headers
Authorization: Bearer eyJhbGci…
Content-Type: application/json
X-Client-Version: 3.2.1
X-Request-ID: 7f4e2d1a-9b3c
Response Headers
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 987
Cache-Control: max-age=300, private
Content-Encoding: gzip
Headers
Request
Response
Timings
{
"data": [
{ "id": 1, "name": "Widget A", "price": 9.99 },
{ "id": 2, "name": "Widget B", "price": 14.99 }
],
"meta": {
"total": 847,
"page": 1,
"per_page": 20,
"last_page": 43
}
}
🔑 What one response reveals The meta block above tells you: 847 total items, 20 per page, 43 pages total. To fetch all items, loop page from 1 to 43. You also know the exact field names (id, name, price) to use in your script.

Step 4 — Replay the Request

Copy as cURL

Right-click any request → Copy as cURL. Pastes a complete terminal command with all headers. Modify query params and run it directly.

Console fetch()

Replay directly in the browser. Session cookies are included automatically — no need to copy auth headers for cookie-based APIs.

Edit and Resend

Right-click any request → Edit and Resend. Change URL, headers, or body in a panel, click Send — no code needed.

Copy as cURL output

# Copied directly from Firefox — modify page=1 to iterate
curl 'https://api.example.com/v2/products?category=electronics&page=1&limit=20' \
  -H 'Authorization: Bearer eyJhbGci…' \
  -H 'Content-Type: application/json' \
  -H 'X-Client-Version: 3.2.1' \
  --compressed

Console replay — GET with Bearer token

const token = localStorage.getItem('access_token');
const r = await fetch('https://api.example.com/v2/products?page=2&limit=20', {
    headers: { 'Authorization': `Bearer ${token}` }
});
const data = await r.json();
console.table(data.data); // renders the array as a sortable table

Console replay — POST request

const token = localStorage.getItem('access_token');
const r = await fetch('https://api.example.com/v2/products', {
    method: 'POST',
    headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        name: 'Test Product',
        price: 9.99,
        category_id: 7
    })
});
const result = await r.json();
console.log('Created:', result);

Step 5 — Identify Patterns

URL naming conventions

PatternStyleMeaning
/api/v1/usersRESTResource list — GET=list, POST=create
/api/v1/users/42RESTSingle resource — GET=read, PUT=update, DELETE=delete
/api/v1/users/42/ordersREST nestedOrders belonging to user 42
/graphqlGraphQLSingle endpoint — all operations in the POST body
/rpc/getUsersRPCOperation name in the URL path
?action=listProductsOld-style RPCOperation name hidden in query params

Pagination patterns

Page-based

?page=1&per_page=20

Most common. Predictable — iterate page 1 to last_page.

Offset-based

?offset=0&limit=20

Same as page-based in absolute terms. offset = (page-1) × limit.

Cursor-based

?cursor=eyJpZCI6NDJ9

Copy cursor from response into the next request. Used for large/real-time datasets.

Token-based

next_page_token

Similar to cursor. Common in Google APIs. Token is opaque — just pass it through.

Step 6 — Find Source Clues in the Debugger

The Network panel shows what was sent. The Debugger shows why — the code that builds the request. This reveals all possible parameters, not just the ones the UI exposes.

Using the Initiator column

In the Network panel, click the Initiator column for any request. It shows the JavaScript file and line that called fetch(). Click it — Firefox opens the Debugger at that exact line. Read the function to discover every parameter it can accept.

// What you find at the call site — reveals hidden parameters
async function loadProducts(filters) {
    const params = new URLSearchParams({
        page:     filters.page || 1,
        limit:    filters.pageSize || 20,
        category: filters.category,
        sort:     filters.sortField + '_' + filters.sortOrder, // "price_asc"
        in_stock: filters.showInStockOnly ? 'true' : undefined
    });
    return fetch(`/api/v2/products?${params}`, {
        headers: { 'Authorization': `Bearer ${getToken()}` }
    });
}

From reading this function you know: sort takes the format field_direction (e.g. price_asc, name_desc), and in_stock=true is a valid filter — information that the UI may never expose but the API fully supports.

Building an API Map

As you observe requests, document them. This is the deliverable — a reference you can use to write scripts, build integrations, or generate OpenAPI spec.

# API Map — api.example.com
BASE URL: https://api.example.com
AUTH : Bearer token — POST /auth/login, stored in localStorage('access_token')
Token exp: 3600s. Refresh: POST /auth/refresh
RATE LIMITS: X-RateLimit-Limit: 1000/hour per token
X-RateLimit-Remaining in every response. 429 when exceeded.
── Endpoints ──────────────────────────
GET /v2/products
params: page(int) limit(int,max:100) category(str) sort(field_dir) in_stock(bool)
returns: { data: Product[], meta: { total, page, per_page, last_page } }
GET /v2/products/:id
returns: { data: Product }
POST /v2/products
body: { name*(str), price*(float), category_id(int), tags(str[]) }
returns: { data: Product } * = required
GET /v2/categories
returns: { data: Category[] } no params

Exercises

Open the Network panel on any web app. Filter to XHR/Fetch, click through the UI, and find a GET request returning a list. Click it → Response tab. What is the top-level JSON structure? Is it a raw array or wrapped in a data or results key?
Press Ctrl + F in the Network panel. Type a value you can see in the UI (a username, a price, a product name). Which request highlighted? Is it the one you expected?
Right-click any XHR/Fetch request → Copy as cURL. Paste it into a terminal. Does it return the same response? Now change one query parameter value and run it again.
Find a request with a pagination parameter (page=1 or offset=0). Replay it in the Console changing the page:
const r = await fetch('URL_WITH_PAGE=2');
const d = await r.json();
console.table(d.data || d);
Is the response a different set of items?
Find a request in the Network panel. Click its Initiator value. Does Firefox open the Debugger at the fetch() call? What function is making the request? Are there any parameters it accepts that the UI never exposes?
Fetch pages 1 through 3 in one Console snippet and combine the results:
const pages = await Promise.all(
    [1,2,3].map(p =>
        fetch(`/api/items?page=${p}&limit=20`)
          .then(r => r.json())
          .then(d => d.data)
    )
);
console.log('Fetched:', pages.flat().length, 'items');
Click any request → Response Headers. Is there an X-RateLimit-Remaining header? If so, what is the limit and how many requests have you used? What happens when it reaches zero?

Key Takeaways

  • Filter to XHR/Fetch — removes HTML/CSS/image noise and shows only API calls.
  • Ctrl + F in the Network panel searches response bodies — the fastest way to find which endpoint returned a specific value.
  • Every request has four tabs: Headers (URL, method, auth), Request (body for writes), Response (data shape), Timings (performance).
  • Copy as cURL (right-click) gives you an instantly runnable command with all headers — paste into a terminal or import into Postman.
  • Console fetch() is faster for iteration — session cookies are included automatically for cookie-authenticated APIs.
  • Edit and Resend (right-click) modifies and re-fires a request without writing any code.
  • The Initiator column links to the JavaScript that built the request — click it to find hidden parameters in the source.
  • Pagination is discoverable from the response meta block — look for total, last_page, cursor, or next_page_token.
  • Rate limit headers (X-RateLimit-Remaining) tell you how many requests remain — read them before writing loops.
  • Use responsibly — reverse-engineering your own app's API is normal engineering. Doing it at scale against services you don't own may violate their Terms of Service.

Course Complete

You have worked through all 11 lessons. The scenario lessons (8–11) are designed to be revisited whenever you encounter those real-world situations. The panel lessons (2–7) are reference material — return to them when you need a reminder of what a specific tool can do.

1
Introduction to DevTools
Panel
2
The Inspector Panel
Panel
3
The Console Panel
Panel
4
The Network Panel & HAR Files
Panel
5
The Debugger Panel
Panel
6
The Storage Panel
Panel
7
The Performance Panel
Panel
8
Scenario: Inspecting Authentication
Scenario
9
Scenario: Troubleshooting Client Access
Scenario
10
Scenario: Debugging a Slow or Broken Page
Scenario
11
Scenario: Reverse-Engineering an API
Scenario