Xeta API Reference
Welcome to the ultimate foundational layer. The Xeta API is organized around REST. Our API has predictable resource-oriented URLs, accepts JSON-encoded request bodies, returns JSON-encoded responses, and uses standard HTTP response codes, authentication, and verbs.
Production Base URL
https://api.xeta.in/v1Sandbox Base URL
https://sandbox.api.xeta.in/v1API Versioning
When backwards-incompatible changes are made to the API, a new, dated version is released. The current version is v1. You can set your API version globally in your Xeta Developer Dashboard.
X-Xeta-Version: 2026-04-15Always pass the version header to ensure your application doesn't break when we push new updates.
Environments
Xeta provides two distinct environments: Production and Sandbox. The Sandbox environment operates exactly like Production but doesn't actually mutate real user data or send actual WebSocket broadcasts.
- Production:
api.xeta.in - Sandbox:
sandbox.api.xeta.in
Authentication
The Xeta API uses API keys to authenticate requests. You can view and manage your API keys in the Xeta Developer Dashboard.
Test mode secret keys have the prefix sk_test_ and live mode secret keys have the prefix sk_live_. Your API keys carry many privileges, so be sure to keep them secure!
HTTPS Required
All API requests must be made over HTTPS. Calls made over plain HTTP will fail.
curl https://api.xeta.in/v1/users/me \
-H "Authorization: Bearer sk_live_98f89..."
OAuth 2.0 Scopes
Scopes provide a way to limit the amount of access that is granted to an access token. For example, an access token issued to a third-party app might be granted read access to Xora posts, but not write access.
Pagination
All top-level API resources have support for bulk fetches via "list" API methods. Xeta utilizes Cursor-based pagination. Offset pagination is not supported as it scales poorly with massive datasets.
Parameters
limitIntegerA limit on the number of objects to be returned (1-100).
cursorStringA cursor for use in pagination. next_cursor defines your place in the list.
"object": "list",
"has_more": true,
"next_cursor": "post_9x8y7z",
"data": [ ... ]
}
Errors
Xeta uses conventional HTTP response codes to indicate the success or failure of an API request.
- 400 - Bad Request
The request was unacceptable, often due to missing a required parameter.
- 401 - Unauthorized
No valid API key provided.
{
"error": {
"type": "invalid_request_error",
"code": "parameter_missing",
"message": "The parameter 'user_id' is required."
}
}Rate Limits
The Xeta API utilizes a Leaky Bucket algorithm. If you exceed the limit, you will receive a 429 Too Many Requests response. Check the HTTP headers for your status.
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1713160000
Webhooks
Xeta uses webhooks to notify your application when an event happens in your account.
Verifying Signatures
Xeta signs the webhook events it sends to your endpoints by including a signature in each event's Xeta-Signature header.
const crypto = require('crypto');
function verifySignature(payload, header, secret) {
const parts = header.split(',');
const timestamp = parts[0].split('=')[1];
const signature = parts[1].split('=')[1];
const signedPayload = \`${timestamp}.${payload}\`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}Xora Social Graph API
Create a Post
/v1/xora/postsCreates a new post on behalf of the authenticated user.
Body Parameters
contentStringRequiredThe text content of the post.
"content": "Deploying Xeta Core! 🚀"
}
Fetch Xora Feed
/v1/xora/feedRetrieves a paginated array of post objects curated by Lumina's neural recommendation engine.
WebSocket Connection
wss://ws.xeta.in/v1/streamEstablish a persistent TCP connection to the Nexus engine.
Platform Internals
Everything under the hood — the real tech powering xeta.in, explained for developers and students.
Tech Stack
Xeta.in is a full-stack TypeScript application. Here's every layer of the stack, from browser to database.
Next.js 16.2 (App Router)React 19 Server + Client Components, Tailwind CSS 4, Framer Motion
Prisma 6.19Schema-first type-safe database client. Generates TypeScript types from schema.prisma
PostgreSQL on Neon CloudServerless PostgreSQL. Auto-pauses on free tier. Region: ap-southeast-1 (Singapore)
Razorpay SubscriptionsRecurring billing API. Webhook-driven subscription lifecycle management
Custom OTP + Session CookiesPhone/email OTP via VerificationToken table. Session stored as httpOnly cookie (xeta_session)
Nodemailer + Gmail SMTPTransactional emails: OTP, password reset, payment receipts, password change alerts
VercelEdge-deployed Next.js. API routes run as Node.js serverless functions
TypeScript (strict mode)End-to-end type safety from API routes to React components
OTP Auth Flow
Xeta uses a passwordless OTP system backed by the VerificationToken table. Here's the complete flow:
Send OTP
POST /api/auth/send-otpGenerates a 6-digit code, stores it with a 10-minute expiry in VerificationToken with identifier = phone/email, sends via SMS/email
Verify OTP
POST /api/auth/verify-otpFinds the token, checks expiry, deletes it (single-use), creates/updates the User record, sets xeta_session httpOnly cookie = user.id (CUID)
Session Check
cookies().get('xeta_session')Every API route reads this cookie to identify the logged-in user. No JWT — pure DB session lookup via Prisma
Password Reset
POST /api/auth/forgot-passwordGenerates a 64-char hex token, stores with identifier = 'pwd_reset:{email}', emails a 5-minute link to /auth/reset-password?token=...&email=...
await fetch('/api/auth/send-otp', {
method: 'POST',
body: JSON.stringify({
identifier: 'user@email.com'
})
});
// Step 2: Verify OTP
await fetch('/api/auth/verify-otp', {
method: 'POST',
body: JSON.stringify({
identifier: 'user@email.com',
token: '482917'
})
});
// Cookie 'xeta_session' is now set
// All future API calls are authenticated
Subscription System
Xeta uses Razorpay's subscription API. Here's the full payment lifecycle:
/api/create-subscriptionCreates a Razorpay subscription for the selected plan_id. Returns subscription object with id (sub_XXXX) used in the checkout modal.
/api/save-subscriptionCalled from the client after Razorpay payment success. Saves to the Subscription table with userId from session cookie, tier, amount, and Razorpay IDs.
/api/billingReturns all subscriptions for the logged-in user, ordered newest first. Used by the Billing & Invoices dashboard page.
/api/cancel-subscriptionSets the active subscription's status to CANCELLED. No refund logic — purely a DB status change.
/api/webhooks/razorpayRazorpay webhook endpoint. Verifies HMAC-SHA256 signature before processing events like subscription.charged.
const { id } = await
fetch('/api/create-subscription', {
method: 'POST',
body: JSON.stringify({ plan_id })
}).then(r => r.json());
// 2. Open Razorpay checkout
new Razorpay({
key: 'rzp_test_...',
subscription_id: id,
handler: async (res) => {
// 3. Save to DB
await fetch('/api/save-subscription', { ... });
}
}).open();
Database Schema
Xeta uses Prisma with PostgreSQL on Neon. The schema lives at prisma/schema.prisma.
- › id (CUID, PK)
- › firstName, lastName, name
- › email (unique), phone (unique)
- › password (bcrypt hashed)
- › role: USER | ADMIN | ARCHITECT
- › isEmailVerified, isPhoneVerified
- › avatar (base64 JPEG string)
- › status: ACTIVE | BANNED | SUSPENDED
- › id (CUID, PK)
- › userId (FK → User)
- › address1 (required)
- › address2, address3 (optional)
- › state, country, pincode
- › isDefault (bool)
- › createdAt, updatedAt
- › id (CUID, PK)
- › userId (FK → User)
- › tier (plan name string)
- › amount (INR integer)
- › razorpaySubId (unique)
- › razorpayPaymentId (unique)
- › status: ACTIVE | INACTIVE | CANCELLED
- › startDate, endDate
- › id (CUID, PK)
- › identifier (email or phone)
- › token (OTP or reset token)
- › expires (DateTime)
- › Unique: [identifier, token]
- › Used for: OTP login (6-digit)
- › Also: pwd_reset:{email} prefix for password resets
For Students
Learn by studying how Xeta is built. Real production patterns — not toy examples.
Quick Start: Clone & Run Xeta Locally
git clone https://github.com/xeta-systems/xeta-corp
cd xeta-corp
# Install dependencies
npm install
# Set environment variables
cp .env.example .env.local
# Fill in DATABASE_URL, SMTP_USER, RAZORPAY keys
# Push the Prisma schema to your DB
npx prisma db push
# Start dev server
npm run dev
# → localhost:3000
Learn: Build Your Own Auth System
Xeta's auth system avoids JWT complexity. Instead it uses a simple database-backed session pattern:
- 01 Generate a random OTP and store it in DB with an expiry time
- 02 User submits OTP → look it up, check expiry → delete it (one-time use)
- 03 Set an httpOnly cookie with the user's ID
- 04 All protected routes read that cookie and look up the user
Why httpOnly cookies?
JavaScript on the page can't read httpOnly cookies — so even if your app has an XSS bug, the attacker can't steal the session token. This is safer than storing tokens in localStorage.
import { prisma } from '@/lib/prisma';
export async function POST(req) {
const { identifier, token } = await req.json();
// Find OTP in database
const otp = await prisma.verificationToken
.findFirst({ where: { identifier, token } });
if (!otp || otp.expires < new Date())
return Response.json({ error: 'Invalid OTP' }, { status: 401 });
// Delete OTP (one-time use)
await prisma.verificationToken.delete({ where: { id: otp.id } });
// Set session cookie
(await cookies()).set('session', user.id, {
httpOnly: true, sameSite: 'lax'
});
}
Learn: Integrate Razorpay in Next.js
Razorpay has two parts: a server-side API (creates subscriptions/orders) and a client-side JS SDK (opens the payment modal). Never put your key_secret on the frontend.
1. Server: Create a subscription
Use your secret key server-side only. Create a subscription with a plan_id and return its id to the client.
2. Client: Open checkout modal
Load Razorpay's JS SDK, initialize with your public key + subscription_id, and call .open(). The handler callback fires on success.
3. Server: Save the payment
After the handler fires, POST to your own API with razorpay_payment_id and razorpay_subscription_id. Verify via webhook for production.
// src="https://checkout.razorpay.com/v1/checkout.js"
async function pay(planId) {
// Get subscription from your API
const { id } = await fetch('/api/create-subscription',
{ method: 'POST', body: JSON.stringify({ plan_id: planId }) }
).then(r => r.json());
const rzp = new window.Razorpay({
key: 'rzp_test_YOUR_KEY',
subscription_id: id,
name: 'Your Company',
handler: async (response) => {
// response has razorpay_payment_id
await saveToDatabase(response);
}
});
rzp.open();
}