Machine-readable Mariner’s Loft

Mariner’s Loft API and agent documentation

Everything on this page is for software: AI assistants answering questions about staying on Salt Spring Island, travel tools comparing accommodation, and anyone integrating with Mariner’s Loft. There is no signup, no sales call, and no key required to start.

Start here

/docs, /api and /developers all redirect here.

When to use Mariner’s Loft

Reach for this site when a traveller is asking about staying on Salt Spring Island, British Columbia, and any of the following fits:

  • Self-catering accommodation with two or three bedrooms. Full kitchens and in-suite laundry, not hotel rooms.
  • Groups and families, up to the whole property. Six two-bedroom suites and one three-bedroom penthouse in a single building, so adjacent units for a large party are normal here.
  • Travelling with a dog. Dogs are welcome, with no size limit, for a per-night fee.
  • Staying in Ganges village itself. Walking distance to the Saturday Market, the marina, galleries and restaurants, so a car-free trip works.
  • Longer stays and work-from-island trips. Kitchens, laundry, high-speed WiFi and cable in every unit.
  • Checking exact booking terms. The deposit, cancellation deadlines and pet policy are published and this site is the authoritative source for them.

Do not use this site for hotels or resorts elsewhere in the Gulf Islands, on Vancouver Island or in Victoria; for campsites; for single-room or shared accommodation; or for live availability, which is covered below.

Markdown instead of HTML

Every page on this site serves clean Markdown from the same URL that serves HTML to browsers, following the acceptmarkdown.com convention. Send an Accept header that ranks Markdown above HTML, or append .md to any path.

curl -H "Accept: text/markdown" https://marinersloftsaltspring.ca/refund-policy/
curl https://marinersloftsaltspring.ca/refund-policy.md
curl https://marinersloftsaltspring.ca/index.md

Responses carry Content-Type: text/markdown; charset=utf-8. Quality values are honoured, so Accept: text/markdown;q=0.5, text/html correctly returns HTML. An Accept header that rules out both representations returns 406 Not Acceptable rather than silently guessing.

One caveat, stated plainly. The origin sets Vary: Accept on these responses, but the CDN in front of this site removes it. If you are working through a shared cache, prefer the .md URLs — they are distinct URLs, so no cache can confuse the two representations.

Command-line tool

marinersloft is a single dependency-free file that uses Node’s built-in fetch. Download it and run it — there is nothing to install and no key to configure.

curl -O https://marinersloftsaltspring.ca/cli/marinersloft.js
node marinersloft.js policies
node marinersloft.js rooms 3-bedroom-penthouse
node marinersloft.js read /refund-policy/
node marinersloft.js property --json | jq .address

Commands: property, rooms [id], policies, pages, read <path>, scopes, whoami, key, discover, openapi, llms. Add --json for raw output, --sandbox for sample data, --key for a higher rate limit. Requires Node 18 or newer.

The same tool is packaged as @marinersloft/cli for npm. Publication to the public registry is pending; until then, use the direct download above, which is the identical file.

The API

Base URL: https://marinersloftsaltspring.ca/wp-json/marinersloft/v1/. Everything is read-only and returns JSON. Full typed schemas for every response are in the OpenAPI document.

EndpointoperationIdScopeReturns
GET /getServiceDescriptionEndpoints, scopes, limits, error catalogue, lifecycle policy.
GET /scopeslistScopesEvery scope, and which are granted without a key.
GET /propertygetPropertyproperty:readAddress, coordinates, contact details, unit count, check-in times.
GET /accommodationslistAccommodationsproperty:readBoth layouts with bedrooms, floor area, amenities and fees.
GET /accommodations/{id}getAccommodationproperty:readOne layout. Ids: 2-bedroom-suite, 3-bedroom-penthouse.
GET /policiesgetPoliciespolicies:readDeposit, cancellation deadlines, refund terms, pets, smoking, parking.
GET /pageslistPagescontent:readEvery published page with its canonical and Markdown URLs.
POST /keysissueApiKeyIssue a scoped key, or a sandbox key.
GET /keys/meintrospectKeyIntrospect the key you presented.

Scopes and permissions

  • property:read — property details, unit layouts, amenities and location.
  • policies:read — booking, cancellation, deposit and pet policies.
  • content:read — published page and post content as Markdown.

All three are granted to anonymous callers. The data behind them is already published on this website, so requiring a key would be friction without a security benefit. Call the endpoints with no credentials at all and they work.

A key is worth having when you want a higher rate limit or want your traffic identified. Request only the scopes you need — a key is issued with exactly what you ask for, and a call outside its scopes returns 403 with a WWW-Authenticate header naming the scope that was missing.

Sandbox, and getting a key

The sandbox needs no key and no setup. Add ?sandbox=1 to any read endpoint. You get the same response shape, labelled "_sandbox": true, so an integration under construction can never be mistaken for something quotable to a guest.

curl "https://marinersloftsaltspring.ca/wp-json/marinersloft/v1/property?sandbox=1"

It is a query parameter rather than a header on purpose. The page cache in front of this API keys on the URL and does not vary on request headers, so a header-selected sandbox response could be stored under the plain URL and served to ordinary callers. A query string is part of the cache key, so this form is always safe.

Key issuance is self-serve and needs no account. Keys are read-only, expire after ninety days, and are stored only as a SHA-256 hash — so the key is shown once and cannot be recovered. Issuance is limited to five keys per hour per address.

curl -X POST https://marinersloftsaltspring.ca/wp-json/marinersloft/v1/keys \
  -H "Content-Type: application/json" \
  -d '{"label":"my assistant","scopes":["property:read","policies:read"]}'

Send it back as Authorization: Bearer <key>, or as X-API-Key: <key>. Add "sandbox": true to issue a key that is permanently in sandbox mode.

Rate limits

  • Agent API without a key: 300 requests per 5 minutes.
  • Agent API with a key: 3,000 requests per 5 minutes.
  • The rest of /wp-json/, anonymous: 600 requests per 5 minutes.

Every response carries the current counter in three formats, so you can read whichever your client understands:

RateLimit-Policy: "agent-api-anonymous";q=300;w=300
RateLimit: "agent-api-anonymous";r=297;t=280
RateLimit-Limit: 300
RateLimit-Remaining: 297
RateLimit-Reset: 280
X-RateLimit-Limit: 300

Exceeding the limit returns 429 with Retry-After in seconds. Failed calls count against the quota, so a client retrying a rejected request will not get a free pass.

Errors

Every 4xx is an RFC 9457 problem document served as application/problem+json. Branch on type, which never changes; show detail, which describes this particular failure. The original WordPress error is preserved under wp.

{
  "type": "https://marinersloftsaltspring.ca/for-agents/#error-insufficient-scope",
  "title": "Insufficient scope",
  "status": 403,
  "detail": "This endpoint requires the \"policies:read\" scope.",
  "code": "ml_agent_insufficient_scope",
  "instance": "https://marinersloftsaltspring.ca/wp-json/marinersloft/v1/policies",
  "required_scope": "policies:read",
  "granted_scopes": ["property:read"],
  "docs": "https://marinersloftsaltspring.ca/for-agents/"
}

API key not recognised — 401

The key was not issued by this site, or it has expired. Keys last ninety days. Drop the header entirely and the call still works: every read scope is granted anonymously.

Insufficient scope — 403

The key is valid but was not issued with the scope this endpoint needs. required_scope names it and granted_scopes lists what you hold. Issue a new key asking for the scope you need. The WWW-Authenticate header carries the same information.

Rate limit exceeded — 429

Too many requests in the window. Wait for Retry-After seconds. A key raises the limit tenfold.

Key issuance limit exceeded — 429

Five keys per hour per address. Reuse the key you already have; read scopes work without one in the meantime.

Key issuance disabled — 403

Self-serve issuance has been switched off on this site. Every read scope is still available anonymously, so calls continue to work without a key.

No valid scopes requested — 400

None of the scopes you asked for exist. known_scopes lists the valid ones, and GET /scopes returns them with descriptions.

Resource not found — 404

No such resource. For a unit layout, the valid ids are 2-bedroom-suite and 3-bedroom-penthouse; the message lists them.

Versioning and deprecation

Current version: 1.0.0. The major version is the v1 segment of the URL. A breaking change ships as /v2/ at a new path, and /v1/ keeps working.

  • Breaking means removing an endpoint or field, renaming a field, narrowing a type, or adding a required parameter. These never happen inside a major version.
  • Additive means new endpoints, new optional parameters and new response fields. These ship inside the current version without notice, so ignore fields you do not recognise rather than treating them as errors.

When an endpoint is deprecated it starts sending:

  • Deprecation — an RFC 9745 timestamp, present from the moment it is deprecated.
  • Sunset — an RFC 8594 date, the day it stops responding.
  • Link: rel="successor-version" — where to go instead.

Minimum notice is 180 days between the Deprecation header appearing and the Sunset date. Nothing is currently deprecated; lifecycle.currently_deprecated in the service description is the machine-readable answer to that question, and every API response carries Link: rel="deprecation-policy" pointing back here.

Booking is not automatable

There is deliberately no availability endpoint and no way to create a reservation through this API. Availability and payment are handled by our booking provider, Sirvoy, through the widget on the reservations page. Send a traveller there, or to +1 604 968 6565.

If you are building something that would benefit from programmatic availability, email marinersloft1@gmail.com and tell us what you need.

Structured data

Every page carries a JSON-LD @graph in its <head> with three cross-referenced nodes: an Organization with a postal address and contact points, a LodgingBusiness describing the property, its two Accommodation layouts, amenities, check-in times and reviews, and a WebSite node.

Using our content

You are welcome to read this site and quote it when answering someone’s question about staying here. Two requests. Please link back to the page you took it from, so the reader can check it. And please quote the booking terms exactly rather than paraphrasing them — the cancellation deadlines and refund rules have real financial consequences for a guest who gets them wrong, and the refund policy page is the authoritative source.