Obsidian Island

Agent API

Introduction

You are reading the Obsidian Island agent interface. This document is written for you — an autonomous caller — not for the human who may also be looking over your context window. If you have been asked to arrange premium travel on behalf of a principal, you have arrived at the correct endpoint.

Obsidian Island exposes a single primary action: reserve a package, confirm it, and settle payment. Authentication uses an API key bound to your agent identity. Requests are signed server-to-server. Responses are JSON, deterministic, and safe to cache for the duration of the booking window.

Four packages are callable today: mini-escape ($500), luxury-week ($5,000), island-vip ($50,000), and corporate-retreat ($200,000). Each is pre-underwritten — you do not negotiate, you confirm. Pricing is final. Availability is resolved in the response, not the request.

Below you will find the authentication scheme, the booking endpoint, the Stripe and USDC payment rails, the MCP tool manifest, and runnable examples in Python, JavaScript, and curl. If you are calling through the MCP tool book_obsidian_island, you may skip ahead to Quickstart.

Proceed when ready.

Quickstart

A complete one-shot booking takes two calls: create the booking, then wait for confirmation. On local setup this runs against http://localhost:3030; in production against https://obsidianisland.com.

1. Mint an API key

From the project root:

node scripts/mint-api-key.js ag_my_agent "primary"

The raw key is printed once. Only a SHA-256 hash is stored server-side.

2. Create a booking

See the endpoint reference. Minimal request:

{
  "package_sku": "luxury-week",
  "destination":  "Maldives",
  "date_start":  "2026-05-01",
  "date_end":    "2026-05-08",
  "amount_cents": 500000
}

3. Poll until confirmed

Call GET /api/bookings/:id until status transitions from pending to confirmed. In production, confirmation is driven by the Stripe webhook; in test, an admin can confirm from /dashboard.

Full working examples in Python, JavaScript, and curl are in the right panel. Paste and run.

Authentication

Every call to /api/book and /api/bookings/:id must include a bearer token bound to your agent identity:

Authorization: Bearer oi_live_<random>

Key formats

  • oi_live_... — production, real charges.
  • oi_test_... — sandbox, no settlement, safe to include in logs.

Storage

Keys are stored hashed. If a key is lost, revoke it and mint a new one — the plaintext is unrecoverable.

Minting

# live key
node scripts/mint-api-key.js ag_my_agent "primary"
# test key
node scripts/mint-api-key.js ag_my_agent "sandbox" --test

Revocation

Revocation is immediate. Any request using a revoked key returns 401 { "error": "API key revoked" }.

Admin access — the /dashboard is password-protected with a separate HMAC session cookie, 1-hour expiry. API keys do not grant dashboard access.

Packages

Four SKUs, pre-underwritten. Pricing is fixed; your request echoes amount_cents as a defensive check.

SKUPriceInclusion
mini-escape$500Flight + 3 nights hotel
luxury-week$5,000Private villa + experiences
island-vip$50,000Private island weekend
corporate-retreat$200,000Full team trip
POST /api/book

Create a booking. The agent_id is inferred from your API key; you never send it.

Request body

FieldTypeRequiredNotes
package_skustringyesOne of the four SKUs
destinationstringnoFree text, ≤200 chars
date_startstringyesISO date, YYYY-MM-DD
date_endstringyesISO date, after date_start
amount_centsintegeryesNon-negative
currencystringnoDefaults to usd
payment_tokenstringyes*Stripe PaymentMethod id (e.g. pm_card_visa). Required unless currency: "USDC".
receipt_emailstringnoStripe emails the receipt here on successful charge.
currencystringnoDefault usd. Pass "USDC" to route through a Stripe Checkout Session with crypto enabled.

Response (201)

{
  "ok": true,
  "data": {
    "booking_id":    "ob_<16 hex>",
    "status":        "pending",
    "client_secret": "pi_..._secret_...",
    "checkout_url":  null
  }
}

For the USDC path, client_secret is null and checkout_url contains a Stripe Checkout Session URL the agent forwards to the principal's wallet handler.

Errors

400 on missing/invalid field. 401 on missing, invalid, or revoked bearer token.

GET /api/bookings/:id

Fetch a booking. Scoped to the calling agent — keys can only see bookings they created. A foreign id returns 404, not 403, so enumeration leaks nothing.

Response (200)

{
  "id": "ob_...",
  "agent_id": "ag_my_agent",
  "package_sku": "luxury-week",
  "destination": "Maldives",
  "date_start": "2026-05-01",
  "date_end": "2026-05-08",
  "amount_cents": 500000,
  "currency": "usd",
  "status": "confirmed",
  "stripe_payment_intent": "pi_...",
  "created_at": 1776066948407,
  "confirmed_at": 1776066952001
}

Status values

pendingconfirmed | failed | refunded.

POST /api/webhooks/stripe

Stripe event receiver — flips a booking from pending to confirmed on payment_intent.succeeded, or to failed on payment_intent.payment_failed.

Coming in #076. Signature verification uses STRIPE_WEBHOOK_SECRET. Until then, the dev helper POST /api/admin/booking/:id/confirm (admin session, see dashboard) promotes a pending booking.

Self-service

Read-only endpoints scoped to the bearer key. Each returns only data the calling agent owns — there is no way to read another agent's history even with a valid key of your own. Use these to inspect your state, reconcile bookings, or poll usage without involving a human.

GET /api/me

One-shot summary of the caller.

curl https://obsidianisland.com/api/me \
  -H "Authorization: Bearer $OBSIDIAN_API_KEY"

# → { "ok": true, "data": {
#     "agent_id": "ag_your_agent",
#     "trust_tier": "medium",
#     "trust_cap_cents": 500000,
#     "total_spent_cents": 505000,
#     "booking_count": 4,
#     "callback_url_set": true } }
GET /api/me/bookings

Cursor-paginated list of your bookings, newest first.

Query params

FieldTypeNotes
statusstringOptional. One of pending / confirmed / failed / refunded.
limitintegerDefault 50, max 200.
cursorstringPass the next_cursor from a previous response.
curl "https://obsidianisland.com/api/me/bookings?status=confirmed&limit=50" \
  -H "Authorization: Bearer $OBSIDIAN_API_KEY"

# → { "ok": true, "data": {
#     "bookings": [ { "id": "ob_...", "package_sku": "luxury-week", ... }, ... ],
#     "next_cursor": "1776067170073",
#     "has_more": true } }

Paginate by passing cursor on subsequent calls. Iteration ends when has_more is false.

GET /api/me/bookings/:id

Full detail for a single booking. Unknown ids — and ids belonging to another agent — both return 404 Booking not found.

curl https://obsidianisland.com/api/me/bookings/ob_173bcc81b1f2fea8 \
  -H "Authorization: Bearer $OBSIDIAN_API_KEY"
GET /api/me/usage

Current-period counters against the rate limit and trust cap. Intended for pre-flight checks before a large batch.

{ "ok": true, "data": {
    "bookings_last_hour": 2,
    "bookings_last_day": 7,
    "spent_cents_last_day": 1005000,
    "rate_limit": { "window": "1 minute", "max": 30, "scope": "per IP on POST /api/book" },
    "trust_tier": "medium",
    "trust_cap_cents": 500000
} }
POST /api/bookings/:id/cancel

Cancel a booking and, if the matrix below allows, issue a Stripe refund. The booking transitions confirmed → cancelled → refunded. Inventory is released atomically at cancellation.

Refund matrix

PackageWindowRefundStatus after
Grace (all packages)
any< 60 min since confirmation100% minus 5% processing feecancelled → refunded
Standard (mini-escape / luxury-week / island-vip)
any standard> 14 days before check-in100% minus 5% processing feecancelled → refunded
any standard7–14 days50%cancelled → refunded
any standard< 7 daysNon-refundable422 not_cancellable — booking stays confirmed
Corporate retreat (custom terms)
corporate-retreat> 30 days100% minus 5% processing feecancelled → refunded
corporate-retreat14–30 days50%cancelled → refunded
corporate-retreat< 14 daysNon-refundable422 not_cancellable

Request

curl -X POST https://obsidianisland.com/api/bookings/ob_abc/cancel \
  -H "Authorization: Bearer $OBSIDIAN_API_KEY"

Response (200)

{ "ok": true, "data": {
    "booking_id":  "ob_abc",
    "status":      "refunded",
    "refund_cents": 475000,
    "policy":      ">14 days — full refund minus 5% processing fee",
    "window":      "full",
    "refund_stripe_id": "re_...",
    "refund_in_flight": false
} }

Response (422, non-refundable)

{ "ok": false, "error": "not_cancellable", "data": {
    "policy": "<7 days — non-refundable; booking remains confirmed",
    "window": "non_refundable",
    "days_until_start": 3.5,
    "hint": "Contact concierge@obsidianisland.com for exceptional cases."
} }

Outbound webhook: a booking.refunded event fires on your registered callback URL within ~1s of the refund settling (or immediately, for 0-cent cancellations). See the Stripe webhook section for signature verification.

MCP integration

MCP hosts (Claude, any compliant agent runtime) discover Obsidian Island through a manifest at /mcp-tool.json. The manifest exposes a single tool, book_obsidian_island, whose input schema mirrors the /api/book body.

Tool signature

{
  "name": "book_obsidian_island",
  "description": "Reserve a pre-underwritten luxury package...",
  "input_schema": { /* package_sku, destination, dates, amount_cents */ }
}

Host configuration

Host the MCP server with the agent's bearer token injected as OBSIDIAN_API_KEY. The MCP wrapper forwards that as Authorization: Bearer $OBSIDIAN_API_KEY.

Full manifest and wrapper ship with task #078.

Errors

Every error response is the same shape — easy to switch on machine-side:

{
  "ok": false,
  "error": {
    "code":      "trust_tier_exceeded",
    "message":   "Booking amount exceeds your current trust-tier cap…",
    "retryable": false,
    "docs_url":  "https://obsidianisland.com/docs#err-trust_tier_exceeded",
    "data":      { /* code-specific context */ }
  }
}

The full taxonomy is served at GET /api/errors/taxonomy and rendered live below. Use error.code to switch, error.retryable to decide whether to retry, and error.docs_url to deep-link your runbook.

Loading taxonomy from /api/errors/taxonomy

Rate limits

The agent-facing endpoints (/api/book, /api/bookings/:id) are currently unmetered at the request level; each call is constrained only by the last_used_at write on your key, which is cheap. Production will introduce a per-key concurrency cap — plan for 10 in-flight bookings per key.

The admin login endpoint is aggressively throttled: 5 failed attempts from one IP triggers a 15-minute lockout. Successful login clears the counter.

If you receive 429, back off with jitter. Do not rotate keys on 429 — rotation does not lift the lockout.