curl --request GET \
--url https://your-site.com/{basePath}/pages \
--header 'X-API-Key: <api-key>' \
--header 'X-Castro-Signature: <api-key>' \
--header 'X-Castro-Timestamp: <api-key>'import requests
url = "https://your-site.com/{basePath}/pages"
headers = {
"X-API-Key": "<api-key>",
"X-Castro-Timestamp": "<api-key>",
"X-Castro-Signature": "<api-key>"
}
response = requests.get(url, headers=headers)
print(response.text)const options = {
method: 'GET',
headers: {
'X-API-Key': '<api-key>',
'X-Castro-Timestamp': '<api-key>',
'X-Castro-Signature': '<api-key>'
}
};
fetch('https://your-site.com/{basePath}/pages', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://your-site.com/{basePath}/pages",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"X-API-Key: <api-key>",
"X-Castro-Signature: <api-key>",
"X-Castro-Timestamp: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://your-site.com/{basePath}/pages"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("X-API-Key", "<api-key>")
req.Header.Add("X-Castro-Timestamp", "<api-key>")
req.Header.Add("X-Castro-Signature", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://your-site.com/{basePath}/pages")
.header("X-API-Key", "<api-key>")
.header("X-Castro-Timestamp", "<api-key>")
.header("X-Castro-Signature", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://your-site.com/{basePath}/pages")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["X-API-Key"] = '<api-key>'
request["X-Castro-Timestamp"] = '<api-key>'
request["X-Castro-Signature"] = '<api-key>'
response = http.request(request)
puts response.read_bodyList published pages
Lets Castro resolve which entity a crawled URL belongs to (bulk updates match by slug). Return every publicly reachable page you want Castro to be able to update.
Castro also uses this endpoint to sync page identity onto its crawl of
your site, so the richer each item is, the better. categories and
tags are optional — omit them and the sync still works, those fields
just stay empty in Castro.
curl --request GET \
--url https://your-site.com/{basePath}/pages \
--header 'X-API-Key: <api-key>' \
--header 'X-Castro-Signature: <api-key>' \
--header 'X-Castro-Timestamp: <api-key>'import requests
url = "https://your-site.com/{basePath}/pages"
headers = {
"X-API-Key": "<api-key>",
"X-Castro-Timestamp": "<api-key>",
"X-Castro-Signature": "<api-key>"
}
response = requests.get(url, headers=headers)
print(response.text)const options = {
method: 'GET',
headers: {
'X-API-Key': '<api-key>',
'X-Castro-Timestamp': '<api-key>',
'X-Castro-Signature': '<api-key>'
}
};
fetch('https://your-site.com/{basePath}/pages', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://your-site.com/{basePath}/pages",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"X-API-Key: <api-key>",
"X-Castro-Signature: <api-key>",
"X-Castro-Timestamp: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://your-site.com/{basePath}/pages"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("X-API-Key", "<api-key>")
req.Header.Add("X-Castro-Timestamp", "<api-key>")
req.Header.Add("X-Castro-Signature", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://your-site.com/{basePath}/pages")
.header("X-API-Key", "<api-key>")
.header("X-Castro-Timestamp", "<api-key>")
.header("X-Castro-Signature", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://your-site.com/{basePath}/pages")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["X-API-Key"] = '<api-key>'
request["X-Castro-Timestamp"] = '<api-key>'
request["X-Castro-Signature"] = '<api-key>'
response = http.request(request)
puts response.read_bodyAuthorizations
The connection key the user generated in Jorge Castro (Settings → Integration → Custom Website). Reject any request whose key doesn't match yours.
Unix time in milliseconds when the request was signed — Castro uses
Date.now(). Reject anything more than a few minutes old.
A seconds-based comparison makes every request look ~55,000 years in the future, and the freshness check then silently passes everything.
HMAC-SHA256("{timestamp}.{rawBody}", api_key), lowercase hex.
rawBody is the exact bytes of the request body — empty for GET
and DELETE, which therefore sign over "{timestamp}.". Hash the raw
bytes, never a re-serialized copy of the parsed JSON: whitespace and key
order differences will break the comparison for some payloads and not
others, which reads as an intermittent bug.
Compare in constant time (crypto.timingSafeEqual, hash_equals,
hmac.compare_digest).
Query Parameters
Items per page. Castro asks for 100.
1-based page number. Castro keeps paging until it receives a page
shorter than per_page, so a full final page must be followed by an
empty one — otherwise it will keep asking.
Response
Page list (bare JSON array)
Your own type label (post, product, page, ...). Castro
maps the well-known ones onto its page classification:
product -> Ecommerce Product Page,
product_cat -> Ecommerce Category Page,
category -> Category Page, post -> Blog Page.
Anything else leaves Castro's own classification intact.
Optional. Category names this page belongs to.
Optional. Tag names on this page.

