Skip to main content

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

  1. Log in to the Joryio dashboard.
  2. Go to Settings → API Keys.
  3. Click Create API Key, pick the scopes the integration needs, and copy the value shown once.
Keep API keys secret

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.

  1. Download the collection
  2. In Postman: Import → drop the file in.
  3. 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

CodeMeaningDescription
200OKRequest succeeded
201CreatedResource created successfully
204No ContentDelete succeeded (empty response body)
400Bad RequestInvalid request parameters
401UnauthorizedInvalid or missing API key
403ForbiddenAPI key lacks required permissions
404Not FoundResource not found
409ConflictResource already exists
429Too Many RequestsRate limit exceeded
500Internal Server ErrorServer error occurred
503Service UnavailableService 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 /users is an upsert keyed on your userId - retrying the same request updates the same profile instead of creating a duplicate.
  • POST /events/track accepts an optional clientEventId; it is used as the stored event ID, so a retried request with the same clientEventId is deduplicated.
  • POST /campaigns/transactional/send accepts an optional idempotencyKey in 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

MethodEndpointDescription
POST/usersCreate or update one user (object body) or many (bare array body, max 1000)
GET/usersList users (limit / offset)
GET/users/searchSearch users by email, name, phone, or ID
GET/users/:userIdGet user by Joryio internal ID
GET/users/by-user-id/:userIdGet user by your userId
PUT/users/:userIdUpdate user by internal ID
PUT/users/by-user-id/:userIdUpdate user by your userId
DELETE/users/:userIdDelete user (returns 204)

Events API

MethodEndpointDescription
POST/events/trackTrack one event (object body) or many (bare array body, max 500)
GET/events/queryQuery events with filters
POST/events/aggregateAggregate event metrics over time

Campaigns API

MethodEndpointDescription
POST/campaignsCreate campaign
GET/campaigns/:idGet campaign
PUT/campaigns/:idUpdate campaign
DELETE/campaigns/:idDelete campaign
POST/campaigns/:id/sendSend campaign
GET/campaigns/:id/statsGet campaign stats

Segments API

MethodEndpointDescription
POST/segmentsCreate segment
GET/segmentsList segments
GET/segments/:idGet segment
PUT/segments/:idUpdate segment
POST/segments/:id/archiveArchive segment (segments cannot be hard-deleted)
GET/segments/:id/usersGet users in segment
GET/segments/:id/sizeGet segment size

Apps API

MethodEndpointDescription
POST/appsCreate app
GET/apps/:idGet app
PUT/apps/:idUpdate app
DELETE/apps/:idDelete app
POST/apps/:id/regenerate-keyRegenerate SDK key
GET/apps/:id/statsGet app statistics

SDKs

For easier integration, use our official SDKs:

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?