{
  "openapi": "3.0.3",
  "info": {
    "title": "RelayStation API",
    "version": "1.0.0",
    "description": "Ephemeral lease primitive for AI agents. Storage, Mailbox, Checkpoint — same primitive, three shortcuts. Handoff is a cross-cutting feature. Hand-derived from app/ route handlers and /api/pricing.endpoints; see the docs Changelog for gaps we are still closing.",
    "contact": {
      "url": "https://docs.relaystation.ai"
    }
  },
  "servers": [
    { "url": "https://relaystation.ai", "description": "Production" }
  ],
  "tags": [
    { "name": "pricing",    "description": "Rates, quotes, and discovery" },
    { "name": "storage",    "description": "Storage lease variant" },
    { "name": "mailbox",    "description": "Mailbox lease variant" },
    { "name": "checkpoint", "description": "Checkpoint lease variant" },
    { "name": "handoff",    "description": "Cross-variant claim-token flow" },
    { "name": "account",    "description": "Account management (dashboard)" }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "rs_live_ API key or account JWT",
        "description": "Single Authorization header covers both rs_live_ API keys (64-hex) and account JWTs (OAuth-issued, 30-day TTL, type=account). See docs/concepts/auth."
      },
      "x402": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Payment",
        "description": "Base64-encoded JSON x402 payment envelope, signed by the caller's wallet. Only accepted by priced lease-creation endpoints."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": { "type": "string" },
          "code":  { "type": "string", "nullable": true },
          "required": {
            "type": "object",
            "description": "Populated on 402 responses — describes the missing payment.",
            "properties": {
              "amount_usdc":    { "type": "number" },
              "wallet_address": { "type": "string" },
              "network":        { "type": "string" }
            }
          }
        }
      },
      "StorageLease": {
        "type": "object",
        "properties": {
          "status":           { "type": "string", "example": "ok" },
          "actor_type":       { "type": "string", "enum": ["account", "wallet"] },
          "lease_id":         { "type": "string" },
          "expires_at":       { "type": "string", "format": "date-time" },
          "duration_seconds": { "type": "integer" },
          "duration_human":   { "type": "string" },
          "size_bytes":       { "type": "integer" },
          "cost_micros":      { "type": "string", "description": "BigInt-as-string" },
          "paid_usdc":        { "type": "number" },
          "tx_hash":          { "type": "string", "nullable": true },
          "from_wallet":      { "type": "string", "nullable": true },
          "ledger_id":        { "type": "string", "nullable": true }
        }
      },
      "Mailbox": {
        "type": "object",
        "properties": {
          "status":         { "type": "string" },
          "lease_id":       { "type": "string" },
          "drop_url":       { "type": "string", "format": "uri" },
          "poll_url":       { "type": "string", "format": "uri" },
          "expires_at":     { "type": "string", "format": "date-time" },
          "capacity_bytes": { "type": "integer" },
          "cost_micros":    { "type": "string" },
          "webhook_secret": {
            "type": "string",
            "nullable": true,
            "description": "Returned exactly once at creation if X-Webhook-Url was supplied. Capture immediately — never returned again."
          }
        }
      },
      "HandoffToken": {
        "type": "object",
        "properties": {
          "status":           { "type": "string" },
          "handoff_token":    { "type": "string" },
          "claim_url":        { "type": "string", "format": "uri" },
          "max_claims":       { "type": "integer" },
          "delete_on_claim":  { "type": "boolean" },
          "expires_at":       { "type": "string", "format": "date-time" },
          "lease_expires_at": { "type": "string", "format": "date-time" }
        }
      },
      "HandoffTokenRow": {
        "type": "object",
        "properties": {
          "handoff_id":       { "type": "string", "description": "Pass to DELETE /api/handoffs/:id to revoke." },
          "token_prefix":     { "type": "string" },
          "max_claims":       { "type": "integer" },
          "claim_count":      { "type": "integer" },
          "claims_remaining": { "type": "integer" },
          "delete_on_claim":  { "type": "boolean" },
          "expires_at":       { "type": "string", "format": "date-time" },
          "expired":          { "type": "boolean" },
          "exhausted":        { "type": "boolean" },
          "created_at":       { "type": "string", "format": "date-time" }
        }
      },
      "LedgerRow": {
        "type": "object",
        "properties": {
          "id":                    { "type": "string" },
          "direction":             { "type": "string", "enum": ["credit", "debit"] },
          "amountMicros":          { "type": "string", "description": "BigInt-as-string" },
          "source":                { "type": "string", "description": "e.g. 'stripe', 'x402', 'internal'" },
          "reason":                { "type": "string" },
          "serviceKey":            { "type": "string", "nullable": true },
          "leaseId":               { "type": "string", "nullable": true },
          "stripePaymentIntentId": { "type": "string", "nullable": true },
          "stripeRefundId":        { "type": "string", "nullable": true },
          "walletAddress":         { "type": "string", "nullable": true },
          "txHash":                { "type": "string", "nullable": true },
          "createdAt":             { "type": "string", "format": "date-time" }
        }
      },
      "ApiKeyRotateResponse": {
        "type": "object",
        "properties": {
          "status":        { "type": "string" },
          "rotated":       { "type": "boolean", "description": "true on rotation (previous key invalidated), false on first issuance" },
          "key":           { "type": "string", "description": "Raw rs_live_... — returned exactly once, never again" },
          "prefix":        { "type": "string" },
          "lastRotatedAt": { "type": "string", "format": "date-time" }
        }
      },
      "CheckoutSession": {
        "type": "object",
        "properties": {
          "status":      { "type": "string" },
          "checkoutUrl": { "type": "string", "format": "uri", "description": "Stripe-hosted checkout URL; redirect the user here" },
          "sessionId":   { "type": "string" },
          "amountCents": { "type": "integer" },
          "packId":      { "type": "string", "nullable": true }
        }
      },
      "AccountSnapshot": {
        "type": "object",
        "properties": {
          "status":              { "type": "string" },
          "account": {
            "type": "object",
            "properties": {
              "id":                  { "type": "string" },
              "email":               { "type": "string", "format": "email" },
              "name":                { "type": "string", "nullable": true },
              "avatarUrl":           { "type": "string", "nullable": true },
              "balanceMicros":       { "type": "string" },
              "suspended":           { "type": "boolean" },
              "apiKeyPrefix":        { "type": "string", "nullable": true },
              "hasGoogle":           { "type": "boolean" },
              "hasGithub":           { "type": "boolean" },
              "hasStripeCustomer":   { "type": "boolean" },
              "welcomeBonusGranted": { "type": "boolean" },
              "createdAt":           { "type": "string", "format": "date-time" },
              "updatedAt":           { "type": "string", "format": "date-time" }
            }
          }
        }
      }
    }
  },
  "paths": {
    "/api/pricing": {
      "get": {
        "tags": ["pricing"],
        "summary": "Live rates, minimums, duration bounds, and the canonical endpoint list.",
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object" } } } }
        }
      }
    },
    "/api/quote": {
      "get": {
        "tags": ["pricing"],
        "summary": "Compute price for a hypothetical Storage lease. No payment required.",
        "parameters": [
          { "name": "size_bytes",       "in": "query", "required": true, "schema": { "type": "integer" } },
          { "name": "duration_seconds", "in": "query", "required": true, "schema": { "type": "integer" } }
        ],
        "responses": {
          "200": { "description": "Quote produced" },
          "400": { "description": "Bad query", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/store": {
      "post": {
        "tags": ["storage"],
        "summary": "Create a Storage lease. Body = raw file bytes.",
        "parameters": [
          { "name": "X-File-Name",        "in": "header", "required": true, "schema": { "type": "string" } },
          { "name": "X-Size-Bytes",       "in": "header", "required": true, "schema": { "type": "integer" } },
          { "name": "X-Duration-Seconds", "in": "header", "required": true, "schema": { "type": "integer" } }
        ],
        "requestBody": {
          "required": true,
          "content": { "application/octet-stream": { "schema": { "type": "string", "format": "binary" } } }
        },
        "security": [{ "bearerAuth": [] }, { "x402": [] }],
        "responses": {
          "200": { "description": "Lease created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StorageLease" } } } },
          "400": { "description": "Bad request",   "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "402": { "description": "Payment required (x402 path)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "500": { "description": "Upstream failure — charge auto-refunded" }
        }
      }
    },
    "/api/download/{lease_id}": {
      "get": {
        "tags": ["storage"],
        "summary": "Download the bytes of a Storage lease you own.",
        "parameters": [
          { "name": "lease_id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": { "description": "Bytes streamed with original filename" },
          "403": { "description": "Not the owner" },
          "410": { "description": "Lease expired or deleted" }
        }
      }
    },
    "/api/leases": {
      "get": {
        "tags": ["storage"],
        "summary": "List leases owned by the authenticated actor.",
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": { "description": "List of leases with access history" }
        }
      }
    },
    "/api/handoff/{lease_id}": {
      "post": {
        "tags": ["handoff"],
        "summary": "Issue a handoff (claim) token against a lease you own. Free at v1 defaults.",
        "parameters": [
          { "name": "lease_id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "max_claims":      { "type": "integer", "default": 1 },
                  "delete_on_claim": { "type": "boolean", "default": false },
                  "ttl_seconds":     { "type": "integer", "default": 3600 }
                }
              }
            }
          }
        },
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": { "description": "Token issued", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HandoffToken" } } } },
          "403": { "description": "Not the owner" },
          "410": { "description": "Lease expired" },
          "501": { "description": "Paid handoff issuance not yet supported — admin tombstone" }
        }
      }
    },
    "/api/handoffs/{lease_id}": {
      "get": {
        "tags": ["handoff"],
        "summary": "List outstanding handoff tokens for a lease you own.",
        "parameters": [
          { "name": "lease_id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": {
            "description": "Token list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status":   { "type": "string" },
                    "lease_id": { "type": "string" },
                    "tokens": { "type": "array", "items": { "$ref": "#/components/schemas/HandoffTokenRow" } }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/handoffs/{id}": {
      "delete": {
        "tags": ["handoff"],
        "summary": "Revoke a handoff token. Idempotent — a second call sees the token gone and returns 404.",
        "description": "Prevents future claims. Outstanding claims already completed are not rolled back. Caller must own the underlying lease via either auth mode (wallet or account).",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": {
            "description": "Revoked",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status":     { "type": "string" },
                    "revoked":    { "type": "boolean" },
                    "handoff_id": { "type": "string" }
                  }
                }
              }
            }
          },
          "403": { "description": "Not the owner" },
          "404": { "description": "Token not found (or already revoked)" }
        }
      }
    },
    "/api/claim/{token}": {
      "get": {
        "tags": ["handoff"],
        "summary": "Redeem a handoff token. No auth — token is the credential.",
        "parameters": [
          { "name": "token", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": { "description": "Underlying resource streamed (Storage only today)" },
          "404": { "description": "Invalid token" },
          "410": { "description": "Token expired / exhausted / lease deleted" },
          "501": { "description": "Paid claim not yet supported — admin tombstone" }
        }
      }
    },
    "/api/mailbox": {
      "post": {
        "tags": ["mailbox"],
        "summary": "Create a Mailbox lease.",
        "parameters": [
          { "name": "X-Capacity-Bytes",   "in": "header", "required": true,  "schema": { "type": "integer" } },
          { "name": "X-Duration-Seconds", "in": "header", "required": true,  "schema": { "type": "integer" } },
          { "name": "X-Webhook-Url",      "in": "header", "required": false, "schema": { "type": "string"  } },
          { "name": "X-Webhook-Secret",   "in": "header", "required": false, "schema": { "type": "string"  } },
          { "name": "X-Max-Message-Bytes","in": "header", "required": false, "schema": { "type": "integer" } },
          { "name": "X-Max-Messages",     "in": "header", "required": false, "schema": { "type": "integer" } }
        ],
        "security": [{ "bearerAuth": [] }, { "x402": [] }],
        "responses": {
          "200": { "description": "Mailbox created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Mailbox" } } } },
          "400": { "description": "Bad request" },
          "402": { "description": "Payment required (x402 path)" }
        }
      }
    },
    "/api/mailboxes": {
      "get": {
        "tags": ["mailbox"],
        "summary": "List the caller's mailboxes with usage aggregates.",
        "security": [{ "bearerAuth": [] }],
        "responses": { "200": { "description": "OK" } }
      }
    },
    "/api/mailbox/{drop_token}/drop": {
      "post": {
        "tags": ["mailbox"],
        "summary": "Anonymous message drop. Token is the credential.",
        "parameters": [
          { "name": "drop_token", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object" } } }
        },
        "responses": {
          "200": { "description": "Dropped" },
          "413": { "description": "Message exceeds cap" },
          "429": { "description": "Rate limited" }
        }
      }
    },
    "/api/mailbox/{mailbox_id}/messages": {
      "get": {
        "tags": ["mailbox"],
        "summary": "Owner lists queued messages.",
        "parameters": [
          { "name": "mailbox_id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "security": [{ "bearerAuth": [] }],
        "responses": { "200": { "description": "OK" } }
      }
    },
    "/api/mailbox/{mailbox_id}/messages/{message_id}": {
      "get": {
        "tags": ["mailbox"],
        "summary": "Owner reads a single message body.",
        "parameters": [
          { "name": "mailbox_id", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "message_id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "security": [{ "bearerAuth": [] }],
        "responses": { "200": { "description": "Message body" } }
      },
      "delete": {
        "tags": ["mailbox"],
        "summary": "Owner deletes a message.",
        "parameters": [
          { "name": "mailbox_id", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "message_id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "security": [{ "bearerAuth": [] }],
        "responses": { "200": { "description": "Deleted" } }
      }
    },
    "/api/checkpoint": {
      "post": {
        "tags": ["checkpoint"],
        "summary": "Create a Checkpoint lease. Body = initial JSON state.",
        "parameters": [
          { "name": "X-Workflow-Id",      "in": "header", "required": true,  "schema": { "type": "string", "pattern": "^[A-Za-z0-9._-]{1,128}$" } },
          { "name": "X-Duration-Seconds", "in": "header", "required": true,  "schema": { "type": "integer" } },
          { "name": "X-Max-Body-Bytes",   "in": "header", "required": false, "schema": { "type": "integer" } }
        ],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object" } } }
        },
        "security": [{ "bearerAuth": [] }, { "x402": [] }],
        "responses": {
          "200": { "description": "Checkpoint created" },
          "413": { "description": "Initial body exceeds X-Max-Body-Bytes" }
        }
      }
    },
    "/api/checkpoints": {
      "get": {
        "tags": ["checkpoint"],
        "summary": "List the caller's checkpoints.",
        "security": [{ "bearerAuth": [] }],
        "responses": { "200": { "description": "OK" } }
      }
    },
    "/api/checkpoint/{workflow_id}": {
      "get": {
        "tags": ["checkpoint"],
        "summary": "Read the latest body.",
        "parameters": [
          { "name": "workflow_id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": {
            "description": "Latest body as JSON. Version + last-written + expires headers are carried in X-Checkpoint-*.",
            "headers": {
              "X-Checkpoint-Version":         { "schema": { "type": "integer" } },
              "X-Checkpoint-Last-Written-At": { "schema": { "type": "string", "format": "date-time" } },
              "X-Checkpoint-Expires-At":      { "schema": { "type": "string", "format": "date-time" } }
            }
          }
        }
      },
      "put": {
        "tags": ["checkpoint"],
        "summary": "Overwrite the body. No additional payment; the reservation covers writes.",
        "parameters": [
          { "name": "workflow_id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object" } } }
        },
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": { "description": "Written" },
          "413": { "description": "Body exceeds max_body_bytes" }
        }
      },
      "delete": {
        "tags": ["checkpoint"],
        "summary": "Delete early. No refund; reservation was prepaid.",
        "parameters": [
          { "name": "workflow_id", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "security": [{ "bearerAuth": [] }],
        "responses": { "200": { "description": "Deleted" } }
      }
    },
    "/v1/account/me": {
      "get": {
        "tags": ["account"],
        "summary": "Snapshot of the authenticated account.",
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AccountSnapshot" } } } }
        }
      }
    },
    "/v1/account/ledger": {
      "get": {
        "tags": ["account"],
        "summary": "Paginated ledger for the authenticated account.",
        "parameters": [
          { "name": "limit",  "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 25 } },
          { "name": "offset", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 0, "default": 0 } },
          { "name": "type",   "in": "query", "required": false, "schema": { "type": "string", "enum": ["topup", "charge", "refund"] } }
        ],
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": {
            "description": "Page of ledger rows",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status":  { "type": "string" },
                    "limit":   { "type": "integer" },
                    "offset":  { "type": "integer" },
                    "hasMore": { "type": "boolean" },
                    "count":   { "type": "integer" },
                    "type":    { "type": "string", "nullable": true, "enum": ["topup", "charge", "refund", null] },
                    "ledger":  { "type": "array", "items": { "$ref": "#/components/schemas/LedgerRow" } }
                  }
                }
              }
            }
          },
          "400": { "description": "Invalid type filter" }
        }
      }
    },
    "/v1/account/api-keys": {
      "get": {
        "tags": ["account"],
        "summary": "List the single active API key prefix (if any).",
        "description": "Option S — single-key-per-account. Returns 0 or 1 entries; the raw key is never returned here.",
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string" },
                    "keys": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "prefix":        { "type": "string" },
                          "lastRotatedAt": { "type": "string", "format": "date-time" }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": ["account"],
        "summary": "Rotate the single API key. Raw key returned exactly once.",
        "description": "201 on first issuance, 200 on rotation. The previous key is invalidated atomically.",
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": { "description": "Rotated (previous key invalidated)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiKeyRotateResponse" } } } },
          "201": { "description": "First issuance",                     "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiKeyRotateResponse" } } } }
        }
      }
    },
    "/v1/account/api-keys/{prefix}": {
      "delete": {
        "tags": ["account"],
        "summary": "Revoke by prefix. 409 on prefix mismatch (stale state).",
        "parameters": [
          { "name": "prefix", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": {
            "description": "Revoked",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status":  { "type": "string" },
                    "revoked": { "type": "boolean" }
                  }
                }
              }
            }
          },
          "404": { "description": "No active key on this account" },
          "409": {
            "description": "Prefix mismatch; refresh and retry",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error":         { "type": "string" },
                    "currentPrefix": { "type": "string" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/billing/checkout": {
      "post": {
        "tags": ["account"],
        "summary": "Create a Stripe Checkout session for topping up the account balance.",
        "description": "Provide exactly one of packId (selects an admin-configured top-up pack) OR amountCents (custom amount, enforced above min_topup_cents). Stripe Customer id is created on first checkout and reused thereafter.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "packId":      { "type": "string" },
                  "amountCents": { "type": "integer", "description": "Custom top-up amount in USD cents. Must be >= min_topup_cents." }
                }
              }
            }
          }
        },
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": { "description": "Session created",         "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CheckoutSession" } } } },
          "400": {
            "description": "Missing body, non-integer amount, or below min_topup_cents (response carries minTopupCents for client display).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error":         { "type": "string" },
                    "minTopupCents": { "type": "integer", "nullable": true }
                  }
                }
              }
            }
          },
          "404": { "description": "Pack not found or inactive" }
        }
      }
    },
    "/v1/auth/google/start": {
      "get": {
        "tags": ["account"],
        "summary": "Begin Google OAuth for account sign-in. Redirects to Google.",
        "responses": { "302": { "description": "Redirect" } }
      }
    },
    "/v1/auth/github/start": {
      "get": {
        "tags": ["account"],
        "summary": "Begin GitHub OAuth for account sign-in. Redirects to GitHub.",
        "responses": { "302": { "description": "Redirect" } }
      }
    }
  }
}
