API Overview
The Joryio REST API provides programmatic access to all platform features.
Base URL
https://api-eu1.joryio.com
All workspaces are currently served from a single region; region-specific endpoints will be documented when additional regions launch.
Authentication
All API requests require authentication via API key. Keys are scoped to a single workspace, carry an explicit permission set, and can be restricted to a list of IPs. See the API Keys reference for the full scope catalog and IP allowlist behavior.
GET /users/by-user-id/user_123
Host: api-eu1.joryio.com
Authorization: Bearer jry_live_your_api_key_here
Content-Type: application/json
Pass the key as a bearer token in the Authorization header on every request. Keys always start with jry_live_ (production) or jry_test_ (test). The key's visible prefix (e.g. jry_live_98f31a72) is safe to log - the rest is secret.
Get your API key
- Log in to the Joryio dashboard.
- Go to Settings → API Keys.
- Click Create API Key, pick the scopes the integration needs, and copy the value shown once.
Never commit API keys to version control or expose them in client-side code. The full value is shown exactly once after creation - store it in your secret manager immediately.
Postman Collection
The fastest way to explore the API: import the official collection into Postman - every public endpoint with an example body, pre-wired to a {{baseUrl}} variable and bearer-token auth.
- Download the collection
- In Postman: Import → drop the file in.
- Set the collection variables:
baseUrl=https://api-eu1.joryio.com,token= your API key (jry_live_...).
Request Format
All requests and responses use JSON:
POST /users
Content-Type: application/json
{
"userId": "user_123",
"email": "user@example.com",
"attributes": {
"plan": "premium"
}
}
Response Format
Endpoints return the resource JSON directly - there is no { "success": true, "data": ... } wrapper envelope.
Success Response
For example, GET /users/by-user-id/user_123 returns the user object itself:
{
"id": "665f1e2a9b3c4d5e6f7a8b9c",
"userId": "665f1e2a9b3c4d5e6f7a8b9c",
"externalId": "user_123",
"email": "user@example.com",
"phone": "+14155550123",
"attributes": { "plan": "premium" },
"createdAt": "2026-01-15T10:30:00.000Z",
"updatedAt": "2026-01-15T10:30:00.000Z"
}
id / userId are Joryio's internal identifier; the identifier you supplied is echoed as externalId.
Error Response
All errors share one shape, produced by a global exception filter:
{
"statusCode": 400,
"message": "Cannot create user without a valid identifier (userId, externalId, or email)",
"timestamp": "2026-01-15T10:30:00.000Z",
"path": "/users"
}
Request-validation failures (400) additionally carry an errors array with one message per failed field:
{
"statusCode": 400,
"message": "Bad Request Exception",
"timestamp": "2026-01-15T10:30:00.000Z",
"path": "/events/track",
"errors": [
"eventName must be shorter than or equal to 500 characters"
]
}
HTTP Status Codes
| Code | Meaning | Description |
|---|---|---|
200 | OK | Request succeeded |
201 | Created | Resource created successfully |
204 | No Content | Delete succeeded (empty response body) |
400 | Bad Request | Invalid request parameters |
401 | Unauthorized | Invalid or missing API key |
403 | Forbidden | API key lacks required permissions |
404 | Not Found | Resource not found |
409 | Conflict | Resource already exists |
429 | Too Many Requests | Rate limit exceeded |
500 | Internal Server Error | Server error occurred |
503 | Service Unavailable | Service temporarily unavailable |
Rate Limiting
Core API endpoints (users, events, segments, campaigns) do not enforce fixed per-endpoint rate limits today. Throttling applies to abuse-prone surfaces - authentication endpoints and inbound webhook receivers - using fixed per-minute windows.
When a request is throttled, the API responds 429 Too Many Requests with a Retry-After header (seconds until the window resets):
HTTP/1.1 429 Too Many Requests
Retry-After: 42
{
"statusCode": 429,
"message": "Too Many Requests",
"timestamp": "2026-01-15T10:30:00.000Z",
"path": "/auth/login"
}
Handling Rate Limits
Limits may be introduced or tightened over time - always treat 429 as retryable, honor Retry-After, and implement exponential backoff:
async function makeRequestWithRetry(url, options, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
const response = await fetch(url, options);
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || Math.pow(2, i);
await sleep(retryAfter * 1000);
continue;
}
return response;
}
}
Pagination
List endpoints paginate with limit / offset query parameters:
GET /users?limit=50&offset=100
Parameters:
limit: Results per page (defaults and maximums vary per endpoint - users: default 50, max 200; segments: default 100, max 100; events query: default 100, max 1000)offset: Number of items to skip (default: 0)
Response:
Paginated list responses wrap the page in a data array plus a pagination object:
{
"data": [...],
"pagination": {
"total": 1234,
"limit": 50,
"offset": 100,
"hasMore": true
}
}
Some endpoints include additional pagination fields (such as page / totalPages), and some smaller lists return a bare JSON array - each endpoint's page documents its exact shape.
Filtering
There is no generic filter[field] or sort query syntax. List endpoints expose endpoint-specific filter parameters instead, for example:
GET /events/query?eventName=Order+Completed&startDate=2026-01-01&endDate=2026-01-31
GET /segments?q=vip&status=active&tags=onboarding
GET /users/search?query=jane
Results are returned in a fixed, most-recent-first order.
Idempotency
There is no generic Idempotency-Key request header. Retry safety is provided per endpoint:
POST /usersis an upsert keyed on youruserId- retrying the same request updates the same profile instead of creating a duplicate.POST /events/trackaccepts an optionalclientEventId; it is used as the stored event ID, so a retried request with the sameclientEventIdis deduplicated.POST /campaigns/transactional/sendaccepts an optionalidempotencyKeyin the body - a retry with the same key will not double-send.
Timestamps
All timestamps are in ISO 8601 format with UTC timezone:
{
"createdAt": "2024-01-15T10:30:00.000Z",
"updatedAt": "2024-01-15T14:45:30.000Z"
}
API Endpoints
Users API
| Method | Endpoint | Description |
|---|---|---|
| POST | /users | Create or update one user (object body) or many (bare array body, max 1000) |
| GET | /users | List users (limit / offset) |
| GET | /users/search | Search users by email, name, phone, or ID |
| GET | /users/:userId | Get user by Joryio internal ID |
| GET | /users/by-user-id/:userId | Get user by your userId |
| PUT | /users/:userId | Update user by internal ID |
| PUT | /users/by-user-id/:userId | Update user by your userId |
| DELETE | /users/:userId | Delete user (returns 204) |
Events API
| Method | Endpoint | Description |
|---|---|---|
| POST | /events/track | Track one event (object body) or many (bare array body, max 500) |
| GET | /events/query | Query events with filters |
| POST | /events/aggregate | Aggregate event metrics over time |
Campaigns API
| Method | Endpoint | Description |
|---|---|---|
| POST | /campaigns | Create campaign |
| GET | /campaigns/:id | Get campaign |
| PUT | /campaigns/:id | Update campaign |
| DELETE | /campaigns/:id | Delete campaign |
| POST | /campaigns/:id/send | Send campaign |
| GET | /campaigns/:id/stats | Get campaign stats |
Segments API
| Method | Endpoint | Description |
|---|---|---|
| POST | /segments | Create segment |
| GET | /segments | List segments |
| GET | /segments/:id | Get segment |
| PUT | /segments/:id | Update segment |
| POST | /segments/:id/archive | Archive segment (segments cannot be hard-deleted) |
| GET | /segments/:id/users | Get users in segment |
| GET | /segments/:id/size | Get segment size |
Apps API
| Method | Endpoint | Description |
|---|---|---|
| POST | /apps | Create app |
| GET | /apps/:id | Get app |
| PUT | /apps/:id | Update app |
| DELETE | /apps/:id | Delete app |
| POST | /apps/:id/regenerate-key | Regenerate SDK key |
| GET | /apps/:id/stats | Get app statistics |
SDKs
For easier integration, use our official SDKs:
- Web SDK: npm install @joryio/web-sdk
- iOS SDK: Swift Package / CocoaPods
- Android SDK: Gradle
- React Native SDK: npm install @joryio/react-native-sdk
Examples
Create User
curl -X POST https://api-eu1.joryio.com/users \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"userId": "user_123",
"email": "user@example.com",
"attributes": {
"firstName": "John",
"lastName": "Doe",
"plan": "premium"
}
}'
Track Event
curl -X POST https://api-eu1.joryio.com/events/track \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"userId": "user_123",
"eventName": "Order Completed",
"properties": {
"orderId": "order_456",
"total": 99.99,
"currency": "USD"
}
}'
Create Segment
curl -X POST https://api-eu1.joryio.com/segments \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Premium Users",
"description": "Users on premium plan",
"filterGroups": [{
"filters": [{
"type": "attribute",
"field": "plan",
"operator": "equals",
"value": "premium"
}],
"operator": "AND"
}],
"groupOperator": "AND"
}'
Send Campaign
The send endpoint takes no request body - the campaign's saved configuration determines what is sent:
curl -X POST https://api-eu1.joryio.com/campaigns/:id/send \
-H "Authorization: Bearer jry_live_your_api_key"
Errors
There is no separate machine-readable error-code vocabulary - use the HTTP status code plus the message field of the standard error body (see Response Format):
{
"statusCode": 404,
"message": "Segment with ID 3f9d2c1e-7a54-4b2e-9c1d-8e6f5a4b3c2d not found",
"timestamp": "2026-01-15T10:30:00.000Z",
"path": "/segments/3f9d2c1e-7a54-4b2e-9c1d-8e6f5a4b3c2d"
}
Testing
Keys can be created with a test label (jry_test_...) so you can tell integration keys apart from production keys at a glance - the label doesn't change what the key can do. For safe experimentation, create a separate workspace for testing: workspaces fully isolate profiles, events, and campaigns, so nothing you try can touch production data or recipients. Channel test sends (email test sends, SMS test messages) are built into the campaign editors.
Support
Need help?