Search docs

Find a documentation page

Lease endpoints

Walkthrough of the trader lease lifecycle: booking, Solana Pay payment, credentials, signing tokens, extensions, ratings, and the payment ledger.

This page documents every lease endpoint with exact request and response shapes, in the order a trader actually uses them: book, pay, read, reveal credentials, mint signing JWTs, then extend, rate, and audit. All shapes come from the generated OpenAPI contract, served live at GET https://api.swqos.dev/v1/openapi.json.

Endpoint summary

MethodPathAuthIdempotency-Key
POST/v1/leasesbearerrequired
GET/v1/leasesbearer
GET/v1/leases/{id}bearer
POST/v1/leases/{id}/extendbearerrequired
PATCH/v1/leases/{id}/auto-renewbearer
POST/v1/leases/{id}/credentialsbearer
POST/v1/leases/{id}/api-keysbearer
POST/v1/leases/{id}/tokenbearer
POST/v1/leases/{id}/ratingbearer
GET/v1/paymentsbearer
GET/v1/solana-pay/{memo}public
POST/v1/solana-pay/{memo}public

"Bearer" means Authorization: Bearer <token> with either a Clerk session JWT (web) or a lease API token. The Solana Pay pair is intentionally unauthenticated: the unguessable ULID memo is the capability, and wallets must be able to call it. See Authentication for token sources and rate limits.

Every authenticated success response is wrapped in the standard envelope {"data": ..., "meta": {"request_id": "<ULID>", "version": "1.0"}}. The Solana Pay pair returns raw bodies (the Solana Pay transaction-request spec defines their shape). Errors are RFC 7807 problem documents served as application/problem+json, catalogued on the Errors page.

Bootstrap: a trader API token only exists after the first lease's one-shot credentials reveal. The first booking therefore happens signed in on the web app (or with a Clerk session JWT). Every call after that can use the lease API token.

Book a lease

POST /v1/leases creates a pending_payment lease for the authenticated trader. The Idempotency-Key header is required: a 26-character Crockford-base32 ULID. Replays within 24 hours return the cached response, a missing key is 400 idempotency-key-missing, a non-ULID key is 400 idempotency-key-malformed, and the same key with a different body is 422 idempotency-key-reused.

curl -X POST https://api.swqos.dev/v1/leases \
  -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: 01JXF69ZD0Q1W2E3R4T5Y6A7B8" \
  -H "Content-Type: application/json" \
  -d '{
    "validator_id": "01JXEZV2P9R8S7T6W5X4Y3Z2A1",
    "epoch_start": 982,
    "epoch_count": 5,
    "mode": "exclusive"
  }'
FieldTypeRequiredDescription
validator_idstringyesULID public_id of the target validator (must be live)
epoch_startintegeryesFirst Solana epoch covered, inclusive
epoch_countintegeryesNumber of epochs, 1 to 30
modestringnoexclusive (default) or shared

exclusive locks the epoch range for this trader alone. shared co-occupies it with up to the validator's seat cap, at the validator's separate shared price. Booking shared against a validator that does not offer it returns 422 shared-not-offered.

A successful booking returns 201 with the payment instructions:

{
  "data": {
    "lease_id": "01JXF6A9QK3ZT8MEWB2C4D5G7H",
    "payment": {
      "solana_pay_url": "solana:https%3A%2F%2Fapi.swqos.dev%2Fv1%2Fsolana-pay%2F01JXF6AB0RCD3EFG4HJK5MN6P7",
      "amount_lamports": 10000000000,
      "memo": "01JXF6AB0RCD3EFG4HJK5MN6P7"
    }
  },
  "meta": {
    "request_id": "01JXF6ACT9K8J7H6G5F4D3S2A1",
    "version": "1.0"
  }
}
FieldDescription
lease_idULID of the new lease, used in every later call
payment.solana_pay_urlSolana Pay transaction-request URL (solana: plus a percent-encoded link). Render it as a QR code or wallet deep link
payment.amount_lamportsExact amount owed in lamports (validator share plus platform fee). The settlement worker rejects anything that credits less
payment.memoSPL memo the payment transaction carries verbatim, the only key correlating the on-chain transfer back to the lease

The lease starts in pending_payment and holds the slot (or shared seat) for 15 minutes. If the payment never confirms in that window the lease flips to expired_unpaid and the slot frees.

Booking errors:

StatusSlugMeaning
400invalid-epoch-start, invalid-epoch-range, lease-total-overflowBad epoch inputs
403cannot-lease-own-validatorYou cannot lease a validator you registered
404validator-not-foundNo live validator with that validator_id
409lease-slot-takenExclusive or cross-mode overlap. extensions.conflicting_lease_id names the blocking lease when resolvable
409shared-capacity-fullEvery shared seat is taken somewhere in the range
422shared-not-offeredValidator does not offer shared mode
429rate-limit-exceededBooking is limited to 10/min per trader

A 409 lease-slot-taken problem document looks like this:

{
  "type": "https://docs.swqos.dev/errors/lease-slot-taken",
  "title": "Lease slot already booked",
  "status": 409,
  "detail": "Requested epoch range overlaps an existing lease for this validator.",
  "extensions": {
    "conflicting_lease_id": "01JXEZWXY2Z3A4B5C6D7E8F9G0"
  }
}

Pay: the Solana Pay pair

Payment is non-custodial. The trader signs one transaction that pays the validator and the platform atomically (a 90/10 split, two SystemProgram::transfer legs plus the SPL memo). The marketplace never holds funds. Both endpoints are public and not enveloped.

GET /v1/solana-pay/

Wallets call this first to render the payment metadata:

curl https://api.swqos.dev/v1/solana-pay/01JXF6AB0RCD3EFG4HJK5MN6P7
{
  "label": "swqos-lease",
  "icon": "https://app.swqos.dev/favicon.ico"
}

POST /v1/solana-pay/

The wallet then posts its account to receive the unsigned transaction:

curl -X POST https://api.swqos.dev/v1/solana-pay/01JXF6AB0RCD3EFG4HJK5MN6P7 \
  -H "Content-Type: application/json" \
  -d '{"account": "7nYabs9dUhvxYwdTnrWVBL9MYviKSfrEbdWmUWHa8oQP"}'
{
  "transaction": "AQAAAAAAAAAAAAAAAAAAAAAA...",
  "message": "swqos lease payment (10000000000 lamports)"
}

transaction is a base64 unsigned legacy transaction with the validator transfer, the platform transfer, and the memo. The wallet signs and submits it.

StatusSlugMeaning
404solana-pay-memo-not-foundMemo is unknown, expired, or already paid. This kills the late-payment race at build time
409payer-is-payout-walletThe paying account is the validator payout wallet, so the validator leg could never be verified. Pay from a different wallet
422invalid-accountaccount is not a base58 32-byte pubkey
503solana-rpc-unavailableBlockhash fetch failed, retry shortly

There is no payment-confirmation endpoint. The settlement worker (Geyser stream plus a catch-up poller) watches the chain, verifies both legs of the finalized transaction, and flips the lease to active. Poll GET /v1/leases/{id} until status changes.

Read leases

GET /v1/leases

Lists the authenticated trader's leases, newest first. Other traders' leases are filtered out at the database.

GET /v1/leases/

Single lease lookup. Returns 404 lease-not-found both when the lease does not exist and when it belongs to another trader (existence is deliberately not disclosed).

Both return LeaseView bodies:

{
  "data": {
    "lease_id": "01JXF6A9QK3ZT8MEWB2C4D5G7H",
    "validator_id": "01JXEZV2P9R8S7T6W5X4Y3Z2A1",
    "mode": "exclusive",
    "epoch_start": 982,
    "epoch_end": 986,
    "price_per_epoch_lamports": 2000000000,
    "total_amount_lamports": 10000000000,
    "status": "active",
    "credentials_revealed": true,
    "auto_renew": false,
    "signs_count": 18243,
    "booked_epoch_count": 5,
    "payment": null,
    "pending_extension_epochs": null,
    "pending_extension_payment": null,
    "refund_amount_lamports": null,
    "revoked_reason": null
  },
  "meta": { "request_id": "01JXF6ACT9K8J7H6G5F4D3S2A1", "version": "1.0" }
}

Status values:

statusMeaning
pending_paymentBooked, slot held, awaiting on-chain confirmation (15-minute window)
activePayment confirmed, credentials and JWTs available
completedLast paid epoch has elapsed
revokedRevoked by the validator or platform. revoked_reason and refund_amount_lamports are populated
expired_unpaidPayment never confirmed in the window, slot released

Field notes:

FieldDescription
paymentSolana Pay instructions, present only while pending_payment so an unpaid booking can be resumed, null once paid or expired
pending_extension_epochs / pending_extension_paymentSet while an extension is awaiting payment
price_per_epoch_lamportsPrice frozen at booking (the exclusive or shared price per mode). Later validator price changes never touch it
booked_epoch_countThe original epoch_count, never mutated by extensions
signs_countLifetime real TEE signing calls served for this lease, the volume that gates rating eligibility
credentials_revealedtrue once the one-shot credentials reveal has been consumed

Reveal credentials (one-shot)

POST /v1/leases/{id}/credentials returns the connection credentials exactly once per lease. The cleartext api_token is only ever returned in this response, the server keeps an Argon2id hash.

curl -X POST https://api.swqos.dev/v1/leases/01JXF6A9QK3ZT8MEWB2C4D5G7H/credentials \
  -H "Authorization: Bearer $TOKEN"
{
  "data": {
    "tee_endpoint_url": "https://tee.validator-example.com:443",
    "api_token": "01JXF6AEYWZ0X1V2T3S4R5Q6P7",
    "tls_ca_cert": "-----BEGIN CERTIFICATE-----\n..."
  },
  "meta": { "request_id": "01JXF6ACT9K8J7H6G5F4D3S2A1", "version": "1.0" }
}
FieldDescription
tee_endpoint_urlgRPC endpoint of the TEE host-proxy for this lease's validator
api_tokenBearer token for all later API calls on this lease. Store it now, it cannot be re-read
tls_ca_certPEM CA bundle the SDK pins for the TLS handshake to the host-proxy
StatusSlugMeaning
404credentials-already-shownThe reveal was already consumed once. Use rotation to recover
404lease-not-foundNo such lease owned by the caller
422lease-not-activeLease is not active yet (for example still pending_payment)

Rotate the API token

POST /v1/leases/{id}/api-keys revokes the current trader token (if any) and mints a fresh one. It works on any active lease whether or not the one-shot reveal happened, so it is the recovery path for a lost token. The response shape is identical to the credentials reveal.

curl -X POST https://api.swqos.dev/v1/leases/01JXF6A9QK3ZT8MEWB2C4D5G7H/api-keys \
  -H "Authorization: Bearer $TOKEN"
StatusSlugMeaning
404lease-not-foundNo such lease owned by the caller
409concurrent-rotationAnother rotation landed first, retry to pick up the latest token
422lease-not-activeLease is not active

Mint signing JWTs

POST /v1/leases/{id}/token is the JWT issuer. Each call mints a fresh ES256 JWT (signed via AWS KMS, kid in the header for rotation). The TEE host-proxy verifies this JWT before serving signing requests. Default TTL is 600 seconds with a hard cap of 3600.

curl -X POST https://api.swqos.dev/v1/leases/01JXF6A9QK3ZT8MEWB2C4D5G7H/token \
  -H "Authorization: Bearer $TOKEN"
{
  "data": {
    "jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ii4uLiJ9...",
    "expires_at": 1781136600
  },
  "meta": { "request_id": "01JXF6ACT9K8J7H6G5F4D3S2A1", "version": "1.0" }
}

expires_at is the exp claim copied out (seconds since the UNIX epoch) so clients can schedule the refresh. Refresh about 60 seconds before expiry. The TypeScript and Python SDKs ship an opt-in background refresher with that 60-second skew: start it with startAutoRefresh() / start_auto_refresh(). The Rust SDK takes a static JWT at connect time and never refreshes it, so Rust clients must mint a fresh token and reconnect before expiry themselves.

Claims carried by the JWT:

ClaimMeaning
issMarketplace issuer URL
subTrader ULID
audThe TEE endpoint URL, prevents cross-TEE replay
val_pkValidator identity pubkey (base58)
lease_idLease public id
epoch_range[epoch_start, epoch_end] inclusive, informational
exp / iat / nbfStandard timestamps, nbf equals iat
jtiUnique ULID per token
quotaOptional { "max_handshakes_per_min": <n> }. Default budget 100/min. Shared leases get the budget split across seats: max(1, floor(budget / seats))
StatusSlugMeaning
403lease-not-activeLease is not active
403lease-epoch-out-of-rangeCurrent Solana epoch is outside [epoch_start, epoch_end]
404lease-not-foundNo such lease owned by the caller
429rate-limit-exceededToken minting is limited to 12/min per identity

Extend a lease

POST /v1/leases/{id}/extend bumps epoch_end forward in place: same lease_id, same credentials, same frozen per-epoch price. The Idempotency-Key header is required. The post-extension total span is capped at 30 epochs.

curl -X POST https://api.swqos.dev/v1/leases/01JXF6A9QK3ZT8MEWB2C4D5G7H/extend \
  -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: 01JXG2BA7CDE6FGH5JKM4NPQ3R" \
  -H "Content-Type: application/json" \
  -d '{"epoch_count": 3}'
{
  "data": {
    "lease_id": "01JXF6A9QK3ZT8MEWB2C4D5G7H",
    "payment": {
      "solana_pay_url": "solana:https%3A%2F%2Fapi.swqos.dev%2Fv1%2Fsolana-pay%2F01JXG2BC8DEF9GHJ0KMN1PQR2S",
      "amount_lamports": 6000000000,
      "memo": "01JXG2BC8DEF9GHJ0KMN1PQR2S"
    },
    "epoch_end": 989
  },
  "meta": { "request_id": "01JXF6ACT9K8J7H6G5F4D3S2A1", "version": "1.0" }
}

The extension mints a fresh memo and goes through the same Solana Pay flow as the booking. Until it is paid, the lease shows pending_extension_epochs and pending_extension_payment, and the JWTs keep working for the already-paid range. An extension whose memo is never paid is rolled back automatically.

StatusSlugMeaning
404lease-not-foundNo such lease owned by the caller
409extension-already-pendingA previous extension is still awaiting payment
409extension-raceA concurrent extension won, retry
409lease-slot-takenThe new range overlaps another lease
422lease-not-activeLease is not active
422lease-expiredLease already ended
422extension-exceeds-capTotal span would exceed 30 epochs

Toggle auto-renew

PATCH /v1/leases/{id}/auto-renew flips the auto_renew flag. Today the flag only records the preference and is echoed back on the lease view. No automated renewal exists yet, so the lease still completes at its last paid epoch whatever the toggle says. To keep a lease running, call POST /v1/leases/{id}/extend before the last epoch elapses. Payment stays non-custodial in any case: the marketplace never auto-charges a wallet.

curl -X PATCH https://api.swqos.dev/v1/leases/01JXF6A9QK3ZT8MEWB2C4D5G7H/auto-renew \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"enabled": true}'
{
  "data": {
    "lease_id": "01JXF6A9QK3ZT8MEWB2C4D5G7H",
    "auto_renew": true
  },
  "meta": { "request_id": "01JXF6ACT9K8J7H6G5F4D3S2A1", "version": "1.0" }
}

Returns 404 lease-not-found when the lease does not exist or belongs to another trader.

Rate a completed lease

POST /v1/leases/{id}/rating records a 1 to 5 star rating with an optional comment (at most 500 characters). One rating per lease. Eligibility is gated: the lease must be completed and must have served real signing volume (the signs_count on the lease view, threshold 10,000 signatures by default).

curl -X POST https://api.swqos.dev/v1/leases/01JXF6A9QK3ZT8MEWB2C4D5G7H/rating \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"stars": 5, "comment": "Solid uptime, zero evictions all week."}'
{
  "data": {
    "lease_id": "01JXF6A9QK3ZT8MEWB2C4D5G7H",
    "rating_id": "01JXH8CD7EFG6HJK5MNP4QRS3T",
    "stars": 5
  },
  "meta": { "request_id": "01JXF6ACT9K8J7H6G5F4D3S2A1", "version": "1.0" }
}
StatusSlugMeaning
400invalid-starsstars outside 1 to 5
400invalid-commentComment longer than 500 characters
404lease-not-foundNo such lease owned by the caller
409already-ratedThis lease already has a rating
422rating-not-yet-eligibleLease not completed yet, or not enough signing volume

Payment ledger

GET /v1/payments returns the trader's settled-payment ledger, newest first. Unlike GET /v1/leases, which collapses extensions into one cumulative range, each row here is a single confirmed on-chain settlement (the initial booking plus every extension) with the exact epoch span it paid for.

curl https://api.swqos.dev/v1/payments \
  -H "Authorization: Bearer $TOKEN"
{
  "data": [
    {
      "lease_id": "01JXF6A9QK3ZT8MEWB2C4D5G7H",
      "validator_id": "01JXEZV2P9R8S7T6W5X4Y3Z2A1",
      "epoch_start": 987,
      "epoch_end": 989,
      "amount_lamports": 6000000000,
      "kind": "extension",
      "confirmed_at": "2026-06-09T14:21:07Z",
      "onchain_tx_signature": "5VERYLongBase58SignatureExample..."
    },
    {
      "lease_id": "01JXF6A9QK3ZT8MEWB2C4D5G7H",
      "validator_id": "01JXEZV2P9R8S7T6W5X4Y3Z2A1",
      "epoch_start": 982,
      "epoch_end": 986,
      "amount_lamports": 10000000000,
      "kind": "booking",
      "confirmed_at": "2026-06-01T09:03:44Z",
      "onchain_tx_signature": "2AnotherBase58SignatureExample..."
    }
  ],
  "meta": { "request_id": "01JXF6ACT9K8J7H6G5F4D3S2A1", "version": "1.0" }
}

kind is "booking" for a lease's first payment and "extension" for each later in-place extend. onchain_tx_signature links to an explorer.

Local development

All examples work against a local stack at http://localhost:8080 (see the backend docker-compose setup). Substitute the base URL, the endpoints and shapes are identical.

Next steps