Connecting
“Connection failed” when I click Connect website
“Connection failed” when I click Connect website
/handshake, or didn’t like the answer. In order
of likelihood:Your base URL isn’t publicly reachable. Castro’s servers call you.
http://localhost:3000 works only on your own machine. Deploy it, or use a
tunnel (ngrok http 3000) while developing.The path is wrong. You give Castro a base URL, not a full endpoint. If
your handshake lives at https://site.com/api/castro/handshake, the base URL
is https://site.com/api/castro. Castro appends /handshake itself.Your server rejected Castro’s own signature. The handshake request is
signed like every other request. If your signature check is broken, the
handshake never even runs. Confirm with:“Handshake verification failed”: my endpoint returned 200
“Handshake verification failed”: my endpoint returned 200
challenge_response was wrong.The classic mistake: signing the challenge with the key that arrived in
the request header instead of your own stored key.HMAC(challenge): the challenge string on its own. No timestamp,
no dot, no body. That formula is only for request signatures.Connection rejected: missing required capabilities
Connection rejected: missing required capabilities
capabilities array must contain both posts.create
and posts.update. An integration that can’t publish or update a post has
nothing to offer Castro, so the connection is refused rather than left in a
half-working state.Authentication
Every request comes back 401, but my key is definitely right
Every request comes back 401, but my key is definitely right
JSON.parse() and re-stringify(), key order and whitespace can shift, and
the HMAC no longer matches. The nasty part: it matches for some payloads,
so it looks intermittent.401 with a timestamp complaint
401 with a timestamp complaint
X-Castro-Timestamp is unix milliseconds. Castro signs with
Date.now(). If you compare it against a seconds-based clock, every request
looks about 55,000 years in the future.time() is seconds: use round(microtime(true) * 1000). In Python,
time.time() is float seconds: use time.time() * 1000.GET and DELETE requests fail, POST and PUT work
GET and DELETE requests fail, POST and PUT work
"{timestamp}.": the timestamp, a dot,
and an empty string. If your code skips the dot when the body is empty,
or signs over the literal string "null" or "undefined", the HMAC won’t
match.Everything worked, then suddenly every request is 401
Everything worked, then suddenly every request is 401
Publishing
Updating a post erased its title, categories or SEO
Updating a post erased its title, categories or SEO
PUT /posts/{id} as a full replace.The update is partial. Apply only the fields present in the body; leave
everything else exactly as it was. Castro’s Update Content action sends
only { content, source_id }. If you overwrite the whole record with that,
the title and every other field become empty.seo deserves the same care: an SEO push carrying only a
description must not wipe the title. Merge the object, don’t replace it.The conformance script asserts this. If it passes, you’re safe.Publishing the same content twice creates duplicate posts
Publishing the same content twice creates duplicate posts
source_id. Every payload for the same piece of Castro content carries
the same source_id, so you can recognise a re-publish:Categories arrive as names, and I expected ids
Categories arrive as names, and I expected ids
tags and author.The post published, but the heading appears twice
The post published, but the heading appears twice
content is HTML with the H1 already removed. Render title as the page
heading yourself. If you’re also injecting the title into the body, you’ll
get it twice.Castro says an action “is not supported by this integration”
Castro says an action “is not supported by this integration”
/handshake response, then click Re-verify connection in
Castro (Settings → Integration → Custom Website → Manage). No key rotation
needed. See capabilities.Page sync
Sync runs but matches nothing
Sync runs but matches nothing
GET /pages against the URLs Castro crawled.
They have to be the same URLs.- Return absolute URLs (
https://site.com/blog/hello), not paths. - Use the canonical form: pick one of
https://vshttp://,wwwvs bare, trailing slash vs none, and be consistent with what your site actually serves. - Make sure Castro has crawled your site at all. Sync stamps identity onto crawled pages; with no crawl, there’s nothing to stamp. Run a crawl first.
Draft pages showed up as live pages on my site
Draft pages showed up as live pages on my site
GET /pages lists published pages only. Castro treats everything you
return as publicly reachable: it crawls those URLs and, if one wasn’t in the
crawl, queues it. Filter drafts out of that list.
