Developer documentation

The XityConnect Platform is a plain HTTP JSON API. There is no SDK to trust — only signatures to verify.

Authentication model

There are no passwords. A client holds an Ed25519 private key and proves possession by signing a server-issued challenge:

POST /auth/challenge?address=xity1…
Returns a one-time challenge string bound to the address. Expires in 300 seconds; consumed on first use.
Sign canonical JSON
The signature payload is exactly Python json.dumps({address, challenge}, sort_keys=True, separators=(',',':')) — i.e. {"address":"xity1…","challenge":"…"} with keys in alphabetical order. Sign with Ed25519; send the signature as hex.
POST /auth/signin
Returns a bearer session token. Attach it as Authorization: Bearer <token> on authenticated calls.

The private key never leaves the client. Registration (/auth/signup) accepts only a client-generated public key in PEM — server-side key handling returns 410 Gone, permanently.

Conventions

  • Base URL: the platform host (default development port 8086).
  • All bodies and responses are JSON (Content-Type: application/json).
  • Errors are FastAPI-shaped: { "detail": "human readable reason" } with a 4xx/5xx status.
  • Unexpected server errors return an opaque reference — quote it when contacting the operator.
  • Browser origins must be allowlisted server-side (XITY_CORS_ORIGINS); the API never allows * with credentials.
  • Addresses are xity1 + 40 hex chars, derived as sha256d(public_key)[:20].

Endpoints

GET/health

Liveness probe.

Response
{ "status": "healthy", "service": "XityConnect Platform", "version": "1.1.0" }
POST/auth/signup

Register an account from a client-generated public key. Creates the wallet, the identity record, and a WALLET_BIND transaction.

Request body
{ "wallet_method": "register", "public_key_pem": "-----BEGIN PUBLIC KEY-----\n…\n-----END PUBLIC KEY-----" }
Response
{ "address": "xity1…", "public_key": "<pem>", "identity_id": "…", "message": "Account registered…" }
POST/auth/challenge?address=xity1…

Issue a one-time auth challenge.

Response
{ "address": "xity1…", "challenge": "<random>", "expires_in": 300 }
POST/auth/signin

Verify the challenge signature and open a session.

Request body
{ "address": "xity1…", "challenge": "<the challenge>", "signature": "<hex>", "device_info": {} }
Response
{ "authenticated": true, "session_token": "…", "expires_at": "…", "address": "xity1…", "risk_level": "LOW" }
POST/auth/signout-all

Revoke every session for the caller.

Auth: Bearer

Response
{ "signed_out": true, "sessions_revoked": 2 }
GET/wallet

The caller's wallet: type, balance, bound identity.

Auth: Bearer

Response
{ "address": "xity1…", "type": "ed25519", "balance": 0, "bound_identity": "…" }
GET/wallet/transactions?limit=50

The caller's transaction history (PII-scrubbed).

Auth: Bearer

Response
{ "transactions": [ { "tx_id": "…", "tx_type": "…", "timestamp": 0 } ] }
GET/identity

The caller's identity record (private evidence excluded).

Auth: Bearer

Response
{ "identity": { "identity_id": "…", "status": "ACTIVE", "verifications": { "email": 1730000000 } } }
POST/verify/request

Issue a verification code for email/phone/device/wallet. The code is logged server-side and relayed by the operator; it is NEVER returned by the API.

Auth: Bearer

Request body
{ "method": "email", "verification_data": { "email": "you@example.com" } }
Response
{ "method": "email", "message": "Verification code issued" }
POST/verify/confirm?method=email&code=…

Confirm a code. On success the method is timestamped on chain. Codes expire in 10 minutes and work once.

Auth: Bearer

Response
{ "verified": true, "method": "email" }
POST/recovery/start

Initiate recovery. Starts the 24-hour clock and notifies the wallet owner. Proofless by design.

Request body
{ "address": "xity1…", "method": "BACKUP_CODE", "recovery_data": {} }
Response
{ "request_id": "<32-byte hex>", "status": "PENDING", "waiting_period_hours": 24 }
GET/recovery/status/{request_id}

Status of a request. The linked address is never disclosed.

Response
{ "request_id": "…", "method": "BACKUP_CODE", "status": "PENDING", "waiting_until": 1730000000 }
GET/recovery/pending

The caller's pending recovery requests.

Auth: Bearer

Response
{ "pending": [ { "request_id": "…", "method": "GUARDIAN", "waiting_until": 0 } ] }
POST/recovery/cancel

Cancel a pending request. Owner-only — requires the owner's session.

Auth: Bearer

Request body
{ "request_id": "…" }
Response
{ "status": "CANCELLED" }
POST/recovery/enroll/guardians?guardians=xity1a&guardians=xity1b

Replace the enrolled guardian set. Each guardian must be a registered wallet; minimum two.

Auth: Bearer

Response
{ "enrolled": 2, "guardians": [ { "address": "xity1…", "enrolled_at": 0 } ] }
GET/recovery/guardians

List the caller's enrolled guardians.

Auth: Bearer

Response
{ "guardians": [ … ] }
POST/recovery/backup-code

Provision (or replace) the caller's single-use backup code. Returned exactly once.

Auth: Bearer

Response
{ "provisioned": true, "backup_code": "<16 hex>", "note": "Store this code securely…" }
POST/recovery/device-token

Provision (or replace) a trusted-device recovery token. Returned exactly once.

Auth: Bearer

Response
{ "provisioned": true, "device_token": "<32 hex>", "note": "…" }
POST/recovery/complete

Complete a recovery after the waiting period with a per-method proof, rotating the wallet key.

Request body
{ "request_id": "…", "proof_data": { "backup_code": "…" }, "new_public_key": "-----BEGIN PUBLIC KEY-----…" }
Response
{ "status": "COMPLETED" }
GET/profile

The caller's presentation profile.

Auth: Bearer

Response
{ "address": "xity1…", "display_name": "" }
PUT/profile

Update display name / preferences / privacy settings.

Auth: Bearer

Request body
{ "display_name": "A. Wan" }
Response
{ "updated": true }
GET/permissions

List the caller's grants in both directions: made by them and held by them.

Auth: Bearer

Response
{ "granted": [ { "id": "…", "grantor": "xity1…", "grantee": "xity1…", "scope": "identity:read", "level": "read", "expires_at": null, "granted_at": 0, "status": "ACTIVE" } ], "received": [ … ] }
POST/permissions/grant

Delegate scoped access. Scopes/levels are a closed allowlist; admin and wildcards are rejected.

Auth: Bearer

Request body
{ "grantee_address": "xity1…", "scope": "identity:read", "level": "read", "expires_in_hours": 24 }
Response
{ "permission_id": "…" }
GET/notifications?unread_only=false

The caller's notifications.

Auth: Bearer

Response
{ "notifications": [ { "id": "…", "event_type": "RECOVERY_INITIATED", "severity": "critical" } ] }
POST/vault/store

Store a claim in the vault. Sealed with a SHA-256 integrity hash verified on every read.

Auth: Bearer

Request body
{ "key": "residency", "value": { "country": "SG" }, "encryption_key_hash": null }
Response
{ "stored": true, "key": "residency", "integrity_hash": "<sha256>" }
GET/vault/{key}

Read a claim. Owner-only; tampered values are refused.

Auth: Bearer

Response
{ "key": "residency", "value": { "country": "SG" } }
GET/chain/info

Chain the platform anchors to.

Response
{ "height": 0, "chain_id": "xity-platform-1", "total_supply": 0, "validator_count": 1 }