The unified analytics and event ingestion API for the Nonito Platform. Track user behavior, manage customer profiles, send messaging templates, and build personalized experiences at scale.
Fast & Async
Asynchronous processing ensures fast response times regardless of database latency.
Easy Integration
Simple REST API works with any language or framework. Get started in minutes.
Secure by Default
Bearer token authentication with legacy API key fallback. Only authorized apps access your data.
Rich User Profiles
Capture default and custom attributes to build complete customer profiles.
Quick Start
Get up and running with the Nonito API in three simple steps.
Get your API Key
API keys are available in the Nonito dashboard.
Include the Authorization header
Add the Authorization: Bearer <token> header to all your requests.
Start tracking
Use /events to track behavior and /user-attributes to manage profiles.
Try it out
curl -X POST https://api.nonito.xyz/events \ -H "Content-Type: application/json" \ -H "Authorization: Bearer your-api-key" \ -d '{"event_id": "550e8400-e29b-41d4-a716-446655440000", "event_name": "page_view", "event_params": {"page": "/home"}}'
Events Endpoint
Track analytics events with automatic deduplication. Processing is asynchronous — the server responds immediately.
https://api.nonito.xyz/eventsIngests a single frontend analytics event. Processing is asynchronous — the server responds immediately and stores the event in a background thread. Deduplication is handled by event_id.
Request Body
| Parameter | Type | Description |
|---|---|---|
event_idRequired | string | Unique identifier for the event. Used for deduplication — generate a UUID on the client side. |
event_nameRequired | string | The name/type of the event (e.g., "page_view", "button_click", "purchase") |
event_paramsOptional | object | Arbitrary JSON object containing event metadata |
User Attributes
Set or update user profile attributes with upsert behavior.
https://api.nonito.xyz/user-attributesCreate or update user profile attributes. New users are created, existing users are updated. Default attributes are overwritten; custom attributes are merged — set a value to null to remove it.
Request Body
| Parameter | Type | Description |
|---|---|---|
user_idRequired | string | Unique identifier for the user |
attributesOptional | object | Object containing user attributes to set |
Default Attributes
These attributes are stored as dedicated columns:
| Attribute | Type | Description |
|---|---|---|
first_name | string | User's first name |
last_name | string | User's last name |
email | string | User's email address |
phone | string | User's phone number |
gender | string | User's gender (m, f, o, u, n, p) |
date_of_birth | string | ISO 8601 date (YYYY-MM-DD) |
country | string | User's country |
language | string | Preferred language |
home_city | string | User's home city |
email_subscription_state | string | opted_in, subscribed, or unsubscribed |
push_subscription_state | string | opted_in, subscribed, or unsubscribed |
Custom Attributes
Any attribute not in the default list is stored as a custom attribute. Custom attributes support strings, numbers, booleans, dates (ISO 8601), arrays, and nested objects. Pass null to remove a custom attribute.
Try it out
curl -X POST https://api.nonito.xyz/user-attributes \ -H "Content-Type: application/json" \ -H "Authorization: Bearer your-api-key" \ -d '{"user_id": "user_123", "attributes": {"first_name": "Alex", "email": "alex@example.com", "vip_level": 5}}'
Send Template
Send a pre-approved WhatsApp template to a recipient.
https://api.nonito.xyz/sendtemplateSend a pre-registered WhatsApp template to a recipient phone number. The template must already be approved at the provider with matching body placeholder count and (if used) header type.
Request Body
| Parameter | Type | Description |
|---|---|---|
api_keyRequired | string | Your API key for authentication |
phone_numberRequired | string | The recipient's phone number in international format (e.g., "201012345678") |
template_nameRequired | string | Name of the registered template (e.g., "welcome_message", "order_dispatched_with_image") |
placeholdersOptional | string[] | Body placeholder values, in order. Must match the count the template was registered with. Omit if the template has no body params. |
languageOptional | string | Template language code (e.g., "en", "ar"). Defaults to "en". Must match the language the template was approved in. |
headerOptional | object | Media header for the template — only valid if the template was registered with a header. Shape:type: "image" | "video" | "document" (sticker is not a valid template header).media_url: public URL of the media file.filename: optional, document headers only. |
Try it out
curl -X POST https://api.nonito.xyz/sendtemplate \ -H "Content-Type: application/json" \ -d '{ "api_key": "your-api-key", "phone_number": "201012345678", "template_name": "order_dispatched_with_image", "placeholders": ["Ali", "ORD-1234"], "language": "en", "header": { "type": "image", "media_url": "https://cdn.example.com/orders/1234.jpg" } }'
Send SMS
Send a single SMS to a recipient.
https://api.nonito.xyz/sms_sendSends a single SMS to one recipient. SMS sending must be enabled for your account, otherwise the request returns 404. Each request is assigned a server-generated message_id (UUID) returned in the response.
Request Body
| Parameter | Type | Description |
|---|---|---|
recipientRequired | string | Recipient phone number in international format (e.g., "201012345678"). Minimum length 6. |
sms_textRequired | string | SMS body. Between 1 and 1600 characters. |
Try it out
curl -X POST https://api.nonito.xyz/sms_send \ -H "Content-Type: application/json" \ -H "Authorization: Bearer your-api-key" \ -d '{ "recipient": "201012345678", "sms_text": "Your order ORD-1234 is on its way!" }'
Response
{
"message": "SMS sent successfully",
"message_id": "550e8400-e29b-41d4-a716-446655440000",
"recipient": "201012345678",
"response": "0"
}OTP Verification
Generate and validate 6-digit one-time passwords for phone verification. Delivers over SMS, email, or WhatsApp, and returns a stable user_id per phone. Codes are stored hashed (HMAC-SHA256) — plaintext is never persisted.
https://api.nonito.xyz/v1/otp/generateGenerates a 6-digit OTP for an Egyptian mobile and delivers it over the chosen channel. Returns a stable user_id that identifies the (tenant, phone) pair on every future call. A re-send is simply another call to this endpoint — it supersedes (invalidates) the previous active code. The code is valid for 5 minutes.
Delivery channels
| Channel | Delivered as | Notes |
|---|---|---|
sms Default | A text message (SMS) to the phone | — |
email | An email to the address you provide | Requires the email field |
whatsapp | A WhatsApp message with the code and a tap-to-copy button | — |
Request Body
| Parameter | Type | Description |
|---|---|---|
phone Required | string | Egyptian mobile in any form (+201…, 201…, 00201…). Normalised to 201XXXXXXXXX. Non-Egyptian numbers return 400 invalid_phone. |
channel Optional | string | "sms" (default), "email", or "whatsapp". |
name Optional | string | Display name (max 120 chars). Used in the email greeting; stored on the identity row. |
email Conditional | string | Required when channel="email". Delivery target only — identity stays phone-based. |
Try it out — WhatsApp
curl -X POST https://api.nonito.xyz/v1/otp/generate \ -H "Content-Type: application/json" \ -H "Authorization: Bearer your-api-key" \ -d '{ "phone": "201009727702", "name": "Alex", "channel": "whatsapp" }'
Response (202 Accepted)
{
"user_id": "82820c9f-9512-4232-b7b6-9bc6a63592fc",
"request_id": "05fbd688-7ec6-48bf-895f-b398186cf1dd",
"expires_at": "2026-06-25T10:51:10.584965+00:00",
"channel": "whatsapp"
}For sms (the default) omit channel. For email, set channel: "email" and include the email field. The user_id is stable: the same phone always returns the same user_id for your tenant.
https://api.nonito.xyz/v1/otp/validateValidates the code the end-user submitted against the most recent active OTP for the phone. Up to 10 wrong attempts are allowed before the row locks. user_id and attempts_used are returned on every response so you can enforce your own caps.
Request Body
| Parameter | Type | Description |
|---|---|---|
phone Required | string | The same phone used at generate (any accepted form). |
otp_code Required | string | Exactly 6 digits. |
Try it out
curl -X POST https://api.nonito.xyz/v1/otp/validate \ -H "Content-Type: application/json" \ -H "Authorization: Bearer your-api-key" \ -d '{ "phone": "201009727702", "otp_code": "123456" }'
Response (200 — valid)
{
"valid": true,
"user_id": "82820c9f-9512-4232-b7b6-9bc6a63592fc",
"attempts_used": 1
}Response statuses
| Status | Endpoint | Meaning |
|---|---|---|
202 | generate | OTP created and delivered. Body echoes channel. |
200 | validate | valid: true — code correct; OTP consumed. |
400 | both | Validation failed (e.g. invalid_phone, missing email, non-6-digit code). |
401 | both | Missing/invalid Bearer token; or on validate, invalid_code (wrong code). |
404 | validate | no_active_otp — no live code for this phone (never sent, expired, or already used). |
410 | validate | expired — the code is older than 5 minutes. |
429 | both | Rate-limited: resend cooldown / hourly / daily cap on generate; max_attempts_exceeded on validate. |
502 | generate | The code was created but could not be delivered on the chosen channel. The detail field describes the failure. Retry after the resend cooldown, or contact Nonito if it persists. |
Batch Event Ingestion
Ingest batches of strictly validated transactional events (orders, customers). Processing is synchronous — the full batch is validated and persisted before responding.
https://api.nonito.xyz/v1/eventsEach event's properties are validated against Pydantic schemas matching the event_name. Persisted via the ingest_event_batch Supabase RPC function. A request_id is returned for tracing.
For automatic Odoo source tagging, use the /v1/odoo/events variant which injects "source": "odoo" into every event.
Request Body
{
"batch": [
{
"event_id": "string (required — unique, for deduplication)",
"user_id": "string (required — E.164 phone number)",
"event_name": "string (required — must be an allowed event type)",
"timestamp": "string (optional — ISO 8601, defaults to now)",
"properties": { }
}
]
}Batch Event Fields
| Field | Type | Description |
|---|---|---|
event_id Required | string | Unique identifier for deduplication |
user_id Required | string | E.164 phone number (e.g., +201234567890) |
event_name Required | string | Must be an allowed event type (see Event Types) |
timestamp Optional | string | ISO 8601 timestamp. Defaults to current time |
properties Required | object | Event-specific data, validated against schema for the event type |
Response (202 Accepted)
{
"status": "queued",
"request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"message": "Successfully queued 1 events."
}Example
curl -X POST https://api.nonito.xyz/v1/events \ -H "Content-Type: application/json" \ -H "Authorization: Bearer your-api-key" \ -d '{"batch": [{"event_id": "ord_001", "user_id": "+201234567890", "event_name": "order_created", "timestamp": "2025-01-15T10:30:00Z", "properties": {"order_id": "ORD-1001", "channel": "online", "financials": {"subtotal": 250, "total_price": 256.5, "currency": "EGP", "payment_method": "visa"}, "customer": {"first_name": "Ahmed", "last_name": "Hassan", "phone": "+201234567890"}, "items": [{"product_id": "SKU-100", "name": "Classic T-Shirt", "quantity": 2, "unit_price": 125, "total_price": 250}]}}]}'
Allowed Event Types
Each event type maps to a specific Pydantic validation schema for the properties object.
| Event Name | Description |
|---|---|
order_created | A new order was placed |
order_updated | An existing order was modified |
order_cancelled | An order was cancelled |
order_packed | An order was packed for shipping |
order_dispatched | An order was dispatched |
order_delivered | An order was delivered |
customer_created | A new customer was registered |
customer_updated | Customer info was updated |
Validation Schemas
Each event type's properties object is validated against these Pydantic models. Invalid properties return a 400 with detailed per-field errors.
order_created
| Field | Type | Description |
|---|---|---|
order_id Required | string | Unique order identifier |
channel Required | string | "online" or "store" |
financials Required | Financials | Payment and pricing details |
branch Conditional | BranchInfo | Required when channel is "store" |
customer Required | CustomerInfo | Customer details |
items Required | OrderItem[] | List of line items |
note Optional | string | Order note |
order_updated
| Field | Type | Description |
|---|---|---|
order_id Required | string | Order identifier |
status Required | string | New order status |
updated_at Optional | string | ISO 8601 timestamp |
note Optional | string | Update note |
financials Optional | Financials | Updated financials |
items Optional | OrderItem[] | Updated line items |
customer Optional | CustomerInfo | Updated customer info |
branch Optional | BranchInfo | Updated branch info |
order_cancelled
| Field | Type | Description |
|---|---|---|
order_id Required | string | Order identifier |
reason Required | string | Cancellation reason |
refunded_amount Optional | float | Amount refunded (default 0.0) |
reference_number Optional | string | Refund transaction ID |
order_packed / order_dispatched
| Field | Type | Description |
|---|---|---|
order_id Required | string | Order identifier |
delivery_company Required | string | Shipping carrier |
tracking_number Required | string | Tracking number |
tracking_link Optional | string | Tracking URL |
note Optional | string | Packing/dispatch note |
order_delivered
| Field | Type | Description |
|---|---|---|
order_id Required | string | Order identifier |
delivery_company Required | string | Shipping carrier (e.g., Bosta, Aramex) |
tracking_number Required | string | Tracking number |
tracking_link Optional | string | Tracking URL |
delivery_agent Optional | string | Delivery agent name |
received_by Optional | string | Person who received the order |
customer_created / customer_updated
| Field | Type | Description |
|---|---|---|
first_name Required | string | Customer first name |
last_name Required | string | Customer last name |
email Optional | string | Customer email |
phone Required | string | Customer phone (E.164) |
marketing_consent Optional | boolean | Marketing opt-in (default false) |
Validation Error Format
{
"error": "Validation Failed",
"message": "One or more events in the batch are invalid.",
"details": [
{
"index": 0,
"errors": [
"Missing required field: 'order_id'",
"financials.total_price: field required"
]
}
]
}Nested Models
Shared data models referenced by the validation schemas above.
Financials
| Field | Type | Default | Description |
|---|---|---|---|
subtotal Required | float | — | Pre-discount subtotal |
total_discounts Optional | float | 0.0 | Total discounts applied |
total_tax Optional | float | 0.0 | Total tax |
total_price Required | float | — | Final price |
currency Optional | string | "EGP" | Currency code |
payment_method Required | string | — | e.g., cash, visa, wallet |
promo_codes Optional | string[] | — | Coupons applied |
BranchInfo
| Field | Type | Description |
|---|---|---|
branch_id Required | string | Branch identifier |
name Required | string | Branch name |
location Optional | string | Physical address or coordinates |
city Optional | string | City / Governorate |
phone Optional | string | Branch contact number |
CustomerInfo
| Field | Type | Description |
|---|---|---|
first_name Required | string | First name |
last_name Required | string | Last name |
email Optional | string | Email address |
phone Required | string | Phone number |
OrderItem
| Field | Type | Default | Description |
|---|---|---|---|
product_id Required | string | — | Product identifier / SKU |
name Required | string | — | Product name |
quantity Required | integer | — | Quantity ordered |
unit_price Required | float | — | Price per unit |
total_discount Optional | float | 0.0 | Discount on this item |
total_tax Optional | float | 0.0 | Tax on this item |
total_price Required | float | — | Final line item price |
category Optional | CategoryInfo | — | Product category |
CategoryInfo
| Field | Type | Description |
|---|---|---|
id Required | string | Category identifier |
name Required | string | Category name |
Authentication
All endpoints require authentication via API key.
API keys are validated against the nonito_api_keys table in Supabase. A valid key resolves to a nonito_id (tenant identifier).
Supported Auth Methods
| Method | Header | Priority |
|---|---|---|
| Bearer Token (standard) | Authorization: Bearer <token> | Primary |
| Legacy | X-API-Key: <token> | Fallback |
Validation Flow
Extract token
From Authorization: Bearer header (primary) or X-API-Key header (fallback).
Query database
Match token against the api_key column in nonito_api_keys table.
Authenticate
If found, the nonito_id is used as the tenant identifier. If not, return 401 Unauthorized.
Example
Authorization: Bearer 550e8400-e29b-41d4-a716-446655440000
Code Examples
Example implementations in popular languages.
Events API
cURL
curl -X POST https://api.nonito.xyz/events \ -H "Content-Type: application/json" \ -H "Authorization: Bearer your-api-key" \ -d '{ "event_id": "550e8400-e29b-41d4-a716-446655440000", "event_name": "button_click", "event_params": { "user_id": "12345", "button_id": "signup_btn", "page": "/home" } }'
JavaScript / TypeScript
const API_KEY = 'your-api-key'; const BASE_URL = 'https://api.nonito.xyz'; const trackEvent = async (eventName, params = {}) => { const response = await fetch(`${BASE_URL}/events`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${API_KEY}`, }, body: JSON.stringify({ event_id: crypto.randomUUID(), event_name: eventName, event_params: params, }), }); return response.json(); }; // Example usage trackEvent('page_view', { page: '/dashboard', user_id: 'u_123', });
Python
import requests import uuid API_KEY = 'your-api-key' BASE_URL = 'https://api.nonito.xyz' HEADERS = { 'Content-Type': 'application/json', 'Authorization': f'Bearer {API_KEY}', } def track_event(event_name, params=None): return requests.post( f'{BASE_URL}/events', headers=HEADERS, json={ 'event_id': str(uuid.uuid4()), 'event_name': event_name, 'event_params': params or {}, }, ).json() # Example usage track_event("user_signup", { "user_id": "12345", "plan": "premium", "source": "google_ads", })
Swift (iOS)
import Foundation func trackEvent(eventName: String, eventParams: [String: Any]) async throws { let url = URL(string: "https://api.nonito.xyz/events")! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("Bearer your-api-key", forHTTPHeaderField: "Authorization") let payload: [String: Any] = [ "event_id": UUID().uuidString, "event_name": eventName, "event_params": eventParams ] request.httpBody = try JSONSerialization.data(withJSONObject: payload) let (data, _) = try await URLSession.shared.data(for: request) print(String(data: data, encoding: .utf8) ?? "") }
User Attributes API
cURL
curl -X POST https://api.nonito.xyz/user-attributes \ -H "Content-Type: application/json" \ -H "Authorization: Bearer your-api-key" \ -d '{ "user_id": "user_123", "attributes": { "first_name": "Alex", "email": "alex@example.com", "country": "United States", "vip_level": 5, "tags": ["premium", "early_adopter"] } }'
JavaScript / TypeScript
const setUserAttributes = async (userId, attributes) => { const response = await fetch('https://api.nonito.xyz/user-attributes', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer your-api-key', }, body: JSON.stringify({ user_id: userId, attributes: attributes, }), }); return response.json(); }; // Set default and custom attributes setUserAttributes('user_123', { first_name: 'Alex', email: 'alex@example.com', vip_level: 5, interests: ['tech', 'gaming'], }); // Unset a custom attribute setUserAttributes('user_123', { vip_level: null, // removes the attribute });
Response
API response format and status codes. Legacy endpoints return 202 Accepted to indicate the request was received and queued for async processing.
Events Response
{
"status": "accepted",
"event_id": "550e8400-e29b-41d4-a716-446655440000"
}User Attributes Response
{
"status": "accepted",
"user_id": "user_123"
}Send Template Response
{
"status": "accepted",
"template_name": "welcome_message"
}Status Codes
Standard Error Format
{
"error": "Error type",
"message": "Human-readable description"
}Common Event Examples
Suggested event names and parameters for common use cases.
| Event Name | Suggested Params | Use Case |
|---|---|---|
page_view | page, referrer, user_id | Track page visits |
button_click | button_id, page, user_id | Track UI interactions |
user_signup | user_id, plan, source | Track new registrations |
purchase | user_id, amount, product_id | Track transactions |
app_open | device, version, user_id | Track app launches |
error | message, stack, user_id | Track errors |