Complete guide to using xltoapi and turning your Google Sheets into REST APIs.
Go to Connections → New connection and paste your Google Sheet URL or ID. Make sure the first row contains column names (headers).
Example URL: https://docs.google.com/spreadsheets/d/abc123/edit
curl https://xltoapi.com/api/v1/sheets/YOUR_CONNECTION_ID \ -H "x-api-key: xlta_your_key_here"
All API requests require an API key in the x-api-key header.
// Required header in all requests x-api-key: xlta_your_key_here
Keys have permissions (scopes): read, write, delete. Each endpoint requires the corresponding scope.
Base URL
https://xltoapi.com/api/v1/sheets/{connectionId}Replace {connectionId} with your connection ID (found in the dashboard).
By default, each connection points to the sheet (tab) configured when it was created. To access other tabs from the same spreadsheet, add /tabs/{tabName} to the URL:
# Default sheet (configured in connection) curl "https://xltoapi.com/api/v1/sheets/ID" \ -H "x-api-key: xlta_YOUR_KEY" # Specific tab curl "https://xltoapi.com/api/v1/sheets/ID/tabs/Sales" \ -H "x-api-key: xlta_YOUR_KEY" # Tab with spaces (URL-encoded) curl "https://xltoapi.com/api/v1/sheets/ID/tabs/Sheet%201" \ -H "x-api-key: xlta_YOUR_KEY"
All sub-routes work the same with /tabs/{tabName}:
| Route | Description |
|---|---|
| /tabs/{tab} | CRUD (GET, POST, PUT, PATCH, DELETE) |
| /tabs/{tab}/info | Column info and metadata |
| /tabs/{tab}/row/{rango} | Row or specific range |
| /tabs/{tab}/search?col=val | Multi-column search |
| /tabs/{tab}/filter/{col}/{val} | Filter by URL |
| /tabs/{tab}/agg/{op}?column=col | Aggregations |
| /tabs/{tab}/pivot/{col} | Pivot table |
Returns the rows of your spreadsheet as JSON. Supports filters, pagination, sorting, and multiple formats.
curl "https://xltoapi.com/api/v1/sheets/ID?limit=10&sort=price&order=desc" \ -H "x-api-key: xlta_..."
Response:
{
"success": true,
"data": [
{ "_row": 2, "name": "Laptop", "price": "1200", "stock": "15" },
{ "_row": 3, "name": "Mouse", "price": "25", "stock": "200" }
],
"meta": { "total": 50, "limit": 10, "offset": 0, "format": "records" }
}The _row field indicates the row number in the sheet (row 1 is the headers, data starts at row 2).
Add one or several rows to the end of your sheet. Send an object for one row or an array for multiple.
// One row
curl -X POST "https://xltoapi.com/api/v1/sheets/ID" \
-H "x-api-key: xlta_..." \
-H "Content-Type: application/json" \
-d '{"name": "Keyboard", "price": "75", "stock": "50"}'
// Multiple rows
curl -X POST "https://xltoapi.com/api/v1/sheets/ID" \
-H "x-api-key: xlta_..." \
-H "Content-Type: application/json" \
-d '[
{"name": "Monitor", "price": "300", "stock": "20"},
{"name": "Webcam", "price": "45", "stock": "80"}
]'Response:
{ "success": true, "data": { "rows_added": 2 } }Replaces the entire row. Fields you don't send will be left empty. You must include _row.
curl -X PUT "https://xltoapi.com/api/v1/sheets/ID" \
-H "x-api-key: xlta_..." \
-H "Content-Type: application/json" \
-d '{"_row": 3, "name": "Mouse Pro", "price": "45", "stock": "150"}'Response:
{ "success": true, "data": { "updated_row": 3, "mode": "full" } }Updates only the fields you send. The rest remain unchanged. You must include _row.
curl -X PATCH "https://xltoapi.com/api/v1/sheets/ID" \
-H "x-api-key: xlta_..." \
-H "Content-Type: application/json" \
-d '{"_row": 3, "price": "55"}'Response:
{ "success": true, "data": { "updated_row": 3, "mode": "partial" } }Only precio is updated. The columns nombre, stock, etc. are not modified.
Deletes a row from the sheet. The row is completely removed (not just cleared).
curl -X DELETE "https://xltoapi.com/api/v1/sheets/ID" \
-H "x-api-key: xlta_..." \
-H "Content-Type: application/json" \
-d '{"_row": 3}'Response:
{ "success": true, "data": { "deleted_row": 3 } }Filter rows by exact value using filter[column]=value. You can combine multiple filters.
# Simple filter GET /api/v1/sheets/ID?filter[category]=Electronics # Multiple filters (AND) GET /api/v1/sheets/ID?filter[category]=Electronics&filter[brand]=Samsung
Use * as a wildcard for partial searches.
filter[name]=*laptop* — Contains 'laptop'filter[name]=Mac* — Starts with 'Mac'filter[name]=*Pro — Ends with 'Pro'# Products containing "pro" (case-insensitive) GET /api/v1/sheets/ID?filter[name]=*pro*&search_ci=true
Use query[column]=__operator(value) for numeric comparisons.
| Operator | Description | Example |
|---|---|---|
__eq(n) | Equal to | query[stock]=__eq(0) |
__ne(n) | Not equal to | query[stock]=__ne(0) |
__gt(n) | Greater than | query[price]=__gt(100) |
__gte(n) | Greater or equal | query[price]=__gte(100) |
__lt(n) | Less than | query[stock]=__lt(10) |
__lte(n) | Less or equal | query[stock]=__lte(10) |
# Products with price > 100 and stock < 10 GET /api/v1/sheets/ID?query[price]=__gt(100)&query[stock]=__lt(10) # Combine with text filters GET /api/v1/sheets/ID?filter[category]=Electronics&query[price]=__lte(500)
Add search_ci=true so that searches are case-insensitive. Applies to exact filters, wildcards, and URL-based search.
# "john", "John", "JOHN" all match GET /api/v1/sheets/ID?filter[name]=john&search_ci=true
limit=10 — Maximum rows to return (default: 100)offset=20 — Skip the first N rows (default: 0)sort=price — Sort by this columnorder=asc|desc — Sort direction (default: asc)# Page 3 with 10 results per page, sorted by price descending GET /api/v1/sheets/ID?limit=10&offset=20&sort=price&order=desc
Sorting is smart: if values are numeric, it sorts numerically (9 < 10). If text, it sorts alphabetically.
Get a specific row or range of rows directly by number.
# Specific row GET /api/v1/sheets/ID/row/2 # Range of rows (2, 3, 4, 5) GET /api/v1/sheets/ID/row/2:5
Response (specific row):
{
"success": true,
"data": { "_row": 2, "name": "Laptop", "price": "1200", "stock": "15" }
}Response (range):
{
"success": true,
"data": [
{ "_row": 2, "name": "Laptop", "price": "1200", "stock": "15" },
{ "_row": 3, "name": "Mouse", "price": "25", "stock": "200" }
]
}The minimum row is 2 (row 1 is the headers).
Simplified form of filtering: /filter/{column}/{value}
# Rows where category = "Electronics" GET /api/v1/sheets/ID/filter/category/Electronics # With case-insensitive GET /api/v1/sheets/ID/filter/name/laptop?search_ci=true
Response:
{
"success": true,
"data": [
{ "_row": 2, "name": "Laptop", "category": "Electronics", "price": "1200" }
],
"meta": { "total": 1 }
}Simplified endpoint for searching by multiple columns. Search parameters are direct (without filter[]).
# Search by name and category GET /api/v1/sheets/ID/search?name=Laptop&category=Electronics # With pagination and case-insensitive GET /api/v1/sheets/ID/search?brand=samsung&search_ci=true&limit=5&sort=price
Reserved parameters (not used as search fields): search_ci, limit, offset, sort, order.
Delete all rows matching the filter. Useful for deleting multiple records at once without knowing row numbers.
# Delete all rows where status = "inactive" curl -X DELETE "https://xltoapi.com/api/v1/sheets/ID/filter/status/inactive" \ -H "x-api-key: xlta_..." # Case-insensitive curl -X DELETE "https://xltoapi.com/api/v1/sheets/ID/filter/status/INACTIVE?search_ci=true" \ -H "x-api-key: xlta_..."
Response:
{ "success": true, "data": { "deleted_rows": 5 } }Calculate mathematical operations on the numeric columns of your sheet.
| Operation | Description |
|---|---|
sum | Total sum |
max | Maximum value |
min | Minimum value |
mean | Average |
median | Median |
count | Count of numeric values |
# Sum of column "price" GET /api/v1/sheets/ID/agg/sum?column=price # Average of all numeric columns GET /api/v1/sheets/ID/agg/mean
Response:
{
"success": true,
"data": {
"operation": "sum",
"column": "price",
"result": { "price": 15750 }
}
}If you don't send column, the operation applies to all columns containing numeric data. Columns without numeric data return null.
Get detailed statistics for each column in your sheet.
GET /api/v1/sheets/ID/info
Response:
{
"success": true,
"data": {
"totalRows": 150,
"totalColumns": 5,
"columns": {
"name": {
"count": 150,
"unique": 120,
"nulls": 0
},
"price": {
"count": 148,
"unique": 45,
"nulls": 2,
"numeric": {
"mean": 350.5,
"std": 125.3,
"min": 10,
"max": 1500,
"p25": 99,
"p50": 250,
"p75": 499
}
}
}
}
}For each column: count (non-empty values), unique (unique values), nulls (empty cells). If the column has numeric data, it includes additional statistics with percentiles.
Group data by a column and apply an aggregation operation. Similar to Excel pivot tables.
_agg=sum|max|min|mean|count — Aggregation operation (default: count)_value=column — Column to operate on (default: all numeric)# Count products by category GET /api/v1/sheets/ID/pivot/category # Sum of sales by region GET /api/v1/sheets/ID/pivot/region?_agg=sum&_value=sales # Average price by brand GET /api/v1/sheets/ID/pivot/brand?_agg=mean&_value=price
Response:
{
"success": true,
"data": {
"groupBy": "category",
"aggregation": "sum",
"valueColumn": "price",
"result": {
"Electronics": { "price": 8500 },
"Clothing": { "price": 3200 },
"Home": { "price": 4050 }
}
}
}Use _format to change the JSON format returned by the main GET.
| Format | Description |
|---|---|
records | Array of objects (default) |
dict | Object of objects: {column: {row: value}}' |
list | Columns as arrays: {column: [values]} |
split | Separated: {columns: [], data: [][], index: []} |
# Dict format
GET /api/v1/sheets/ID?_format=dict
// { "name": { "2": "Laptop", "3": "Mouse" }, "price": { "2": "1200", "3": "25" } }
# List format
GET /api/v1/sheets/ID?_format=list
// { "name": ["Laptop", "Mouse"], "price": ["1200", "25"] }
# Split format
GET /api/v1/sheets/ID?_format=split
// { "columns": ["name", "price"], "data": [["Laptop", "1200"], ["Mouse", "25"]], "index": [2, 3] }Add _raw=1 to get data as an array of arrays, without the success/data/meta wrapping. The first row is the headers.
GET /api/v1/sheets/ID?_raw=1
Response:
[ ["name", "price", "stock"], ["Laptop", "1200", "15"], ["Mouse", "25", "200"], ["Keyboard", "75", "50"] ]
Can be combined with filters, pagination, and sorting: ?_raw=1&sort=precio&order=desc&limit=5
Insert a table with your sheet's data on any web page with a single line of code. No dependencies, no frameworks — just a '<'script'>'.
<!-- Add this where you want to show the table --> <div id="xltoapi-table"></div> <script src="https://xltoapi.com/api/v1/embed?id=YOUR_CONNECTION_ID&key=xlta_your_key"></script>
id — (required) Your connection IDkey — (required) Your API keytheme=light|dark — Visual theme (default: light)limit=100 — Maximum rows to displaytitle=My data — Optional title above the tabletarget=my-div — HTML element ID to render in (default: xltoapi-table)Example with dark theme and title:
<div id="products"></div> <script src="https://xltoapi.com/api/v1/embed?id=abc123&key=xlta_xxx&theme=dark&title=Products&target=products&limit=20"></script>
Full example:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>My page</title> </head> <body> <h1>Inventory</h1> <p>Real-time data from Google Sheets:</p> <div id="xltoapi-table"></div> <script src="https://xltoapi.com/api/v1/embed?id=YOUR_ID&key=xlta_xxx&theme=light&title=Inventory&limit=50"></script> </body> </html>
All GET responses include headers with useful information.
| Header | Description |
|---|---|
X-RateLimit-Limit | Request limit per minute |
X-RateLimit-Remaining | Remaining requests this minute |
X-RateLimit-Reset | Timestamp (epoch) when the counter resets |
X-Sheet-Rows | Total data rows in the sheet |
X-Sheet-Columns | Total columns in the sheet |
Each plan has request limits per minute and per month. When you exceed the limit, you'll receive a 429 error.
| Plan | Per minute | Per month |
|---|---|---|
| Free | 30 | 1,000 |
| Pro | 120 | 50,000 |
| Enterprise | 600 | 500,000 |
// Error 429
{
"success": false,
"error": "Rate limit exceeded",
"mensaje": "Rate limit exceeded"
}Copy this template to display data from your API on any web page:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My data</title>
<style>
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background: #f5f5f5; font-weight: 600; }
tr:hover { background: #f9f9f9; }
</style>
</head>
<body>
<h1>My data</h1>
<table>
<thead><tr id="headers"></tr></thead>
<tbody id="body"></tbody>
</table>
<script>
const API_URL = "https://xltoapi.com/api/v1/sheets/YOUR_ID";
const API_KEY = "xlta_your_key";
fetch(API_URL, { headers: { "x-api-key": API_KEY } })
.then(res => res.json())
.then(({ data }) => {
if (!data.length) return;
const keys = Object.keys(data[0]).filter(k => k !== "_row");
document.getElementById("headers").innerHTML =
keys.map(k => `<th>${k}</th>`).join("");
document.getElementById("body").innerHTML =
data.map(row =>
"<tr>" + keys.map(k => `<td>${row[k] ?? ""}</td>`).join("") + "</tr>"
).join("");
});
</script>
</body>
</html>| Method | Endpoint | Description |
|---|---|---|
| GET | /sheets/{id} | List rows with filters |
| POST | /sheets/{id} | Add rows |
| PUT | /sheets/{id} | Replace full row |
| PATCH | /sheets/{id} | Partial update |
| DELETE | /sheets/{id} | Delete row by _row |
| GET | /sheets/{id}/row/{n} | Row by index |
| GET | /sheets/{id}/row/{n:m} | Row range |
| GET | /sheets/{id}/filter/{col}/{val} | Filter by URL |
| DELETE | /sheets/{id}/filter/{col}/{val} | Delete by filter |
| GET | /sheets/{id}/search | Multi-column search |
| GET | /sheets/{id}/agg/{op} | Aggregations |
| GET | /sheets/{id}/info | Column statistics |
| GET | /sheets/{id}/pivot/{col} | Pivot table |
| GET | /embed?id=...&key=... | Embeddable JS SDK |