Skip to main content

Canvas API Overview

Manage Canvas journeys - multi-step automations built in the visual journey builder - via the REST API.

All endpoints on this page are relative to the base URL: https://api-eu1.joryio.com - see API Overview.

Path is /journeys

Canvas endpoints live under the /journeys path - the API resource name for a canvas is a journey. canvasId and journey id refer to the same identifier.

Authentication

All requests require API key authentication:

Authorization: Bearer jry_live_your_api_key_here
Content-Type: application/json

Required scopes per endpoint group:

ScopeEndpoints
canvas:readList, get, executions, stats, node analytics, versions
canvas:writeCreate, update, duplicate, archive, tags, variants, experiments
canvas:activateActivate, pause, resume, publish, rerun, enter user, rollback
canvas:deleteDelete, bulk delete

List Journeys

GET /journeys

Query Parameters

ParameterTypeDefaultDescription
statusstring-Filter by status
tagsstring-Filter by tags (comma-separated)
qstring-Free-text search
createdBy / editedBystring-Filter by creator / last editor (comma-separated user ids)
pagenumber1Page number
limitnumber-Results per page (max 100)

Example Request

curl -X GET "https://api-eu1.joryio.com/journeys?status=active&limit=20" \
-H "Authorization: Bearer jry_live_your_api_key"

Response

{
"data": [
{ "id": "3c9d2f1a-5e8b-4a7c-9f0d-1b2a3c4d5e6f", "name": "Welcome journey", "status": "active" }
],
"pagination": {
"total": 8,
"page": 1,
"limit": 20,
"offset": 0,
"totalPages": 1,
"hasMore": false
}
}

Create Journey

POST /journeys

Request Body

FieldTypeRequiredDescription
namestringYesJourney name (max 255 chars)
descriptionstringNoDescription (max 1000 chars)
nodesarrayNoGraph nodes (max 500). A wizard-created draft may start empty
edgesarrayNoGraph edges (max 1000)
entryTriggerobjectNoHow users enter - see below
variantsarrayNoWhole-journey A/B/n variants - see below
settingsobjectNotimezone, quietTime, conversionTracking, reEntryPolicy, personalizedVariants + config
sendTypestringNoimmediate, scheduled, recurring, or trigger
scheduledAtstringNoISO 8601 date for sendType: "scheduled"
recurringScheduleobjectNofrequency (daily/weekly/monthly/custom), cron, dayOfWeek, dayOfMonth, timeOfDay, timezone, endDate, maxOccurrences
targetingobjectNoAudience for scheduled/immediate journeys: userIds, filterGroups, excludeFilterGroups, filterOperator, subscriptionPreference
exitCriteriaobjectNoSame filter shape as targeting; matching active users are exited
tagsstring[]NoTags

Node structure

Each entry in nodes has id, type, config (type-specific), and optional position (x/y for the editor). Valid type values:

trigger, delay, condition, behavior_split, context, message,
email, sms, push, in_app, in_app_message, whatsapp, whatsapp_message,
webhook, update_user, connector, experiment, ai_decision,
wallet, update_wallet

Each entry in edges has id, source, target, and optional sourceHandle / targetHandle / label - sourceHandle selects the output port on multi-output nodes (branch groups, behavior-split did / timed_out).

Entry trigger

entryTrigger has a type and a type-specific config. Valid types: event, segment, api, entity_change, whatsapp_inbound, sms_inbound, viber_inbound, attribute_change, subscription_status, schedule.

Whole-journey variants

Each entry in variants: id, name, percentage (0-100; all variants must sum to 100), isControl (a control variant is a measured holdout with no flow), and triggerNodeId (the trigger node this treatment variant roots at). See journey analytics for how variant results are reported.

Example Request

curl -X POST https://api-eu1.joryio.com/journeys \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Welcome journey",
"sendType": "trigger",
"entryTrigger": {
"type": "event",
"config": { "eventName": "signed_up" }
},
"nodes": [
{ "id": "n1", "type": "trigger", "config": { "type": "event", "eventName": "signed_up" } },
{ "id": "n2", "type": "delay", "config": { "delayType": "duration", "value": 1, "unit": "days" } },
{ "id": "n3", "type": "email", "config": { "subject": "Welcome!", "html": "<p>Hi {{ user.firstName }}</p>" } }
],
"edges": [
{ "id": "e1", "source": "n1", "target": "n2" },
{ "id": "e2", "source": "n2", "target": "n3" }
]
}'

Returns the created journey object (status draft).


Get Journey

GET /journeys/:canvasId
curl -X GET https://api-eu1.joryio.com/journeys/3c9d2f1a-5e8b-4a7c-9f0d-1b2a3c4d5e6f \
-H "Authorization: Bearer jry_live_your_api_key"

Returns the full journey including nodes, edges, entryTrigger, variants, and settings.


Update Journey

PUT /journeys/:canvasId

The body accepts the same fields as Create Journey - all optional; only the provided fields are updated.

curl -X PUT https://api-eu1.joryio.com/journeys/3c9d2f1a-5e8b-4a7c-9f0d-1b2a3c4d5e6f \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "name": "Welcome journey v2" }'

Delete Journey

DELETE /journeys/:canvasId

Returns 204 No Content. Journeys with run history should be archived instead (POST /journeys/:canvasId/archive), which halts enrollment and sending while preserving analytics.


Status Endpoints

MethodPathScopeDescription
POST/journeys/:canvasId/activatecanvas:activateActivate - users can start entering
POST/journeys/:canvasId/pausecanvas:activatePause - stops new entries and advancement
POST/journeys/:canvasId/resumecanvas:activateResume a paused journey
POST/journeys/:canvasId/archivecanvas:writeArchive (halts enrollment/sending, keeps history)
POST/journeys/:canvasId/unarchivecanvas:writeRestore to a non-running state; activate explicitly to resume
POST/journeys/:canvasId/publishcanvas:activatePublish the current draft as a new version. Body: optional changeSummary, userTransition (keep_on_version / force_exit / migrate_to_new)
POST/journeys/:canvasId/reruncanvas:activateRe-run the current published version without creating a new one
curl -X POST https://api-eu1.joryio.com/journeys/3c9d2f1a-5e8b-4a7c-9f0d-1b2a3c4d5e6f/activate \
-H "Authorization: Bearer jry_live_your_api_key"

Enter a User (API trigger)

Enroll a specific user into a journey - the entry path for entryTrigger.type: "api".

POST /journeys/:canvasId/enter/:userId

Body (optional): context - a JSON object made available to the execution (readable in messages and conditions as journey context variables).

curl -X POST https://api-eu1.joryio.com/journeys/3c9d2f1a-5e8b-4a7c-9f0d-1b2a3c4d5e6f/enter/user_123 \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "context": { "source": "crm-sync", "priority": "high" } }'

Executions and Analytics

MethodPathDescription
GET/journeys/:canvasId/executionsList executions (status, limit max 100, offset)
GET/journeys/:canvasId/executions/:executionIdGet one execution
GET/journeys/:canvasId/executions/:executionId/contextExecution context variables with inferred types
GET/journeys/:canvasId/statsJourney-level stats (startDate, endDate)
GET/journeys/:canvasId/node-analyticsPer-node analytics (startDate, endDate)
GET/journeys/:canvasId/variant-statsWhole-journey variant performance (A/B/n + control)
GET/journeys/:canvasId/experiments/:nodeId/statsExperiment node stats with significance testing
POST/journeys/:canvasId/experiments/:nodeId/declare-winnerDeclare a winner (body: pathId)
POST/journeys/:canvasId/experiments/:nodeId/resetReset an experiment to collecting
GET/journeys/:canvasId/personalization-statusPersonalized-variants phase status

Additional Endpoints

MethodPathDescription
PUT/journeys/:canvasId/variantsSet whole-journey variants (body: variants array)
GET/journeys/:canvasId/versionsList versions
GET/journeys/:canvasId/versions/compare?v1=&v2=Diff two versions
GET/journeys/:canvasId/versions/compare-draftDiff current draft against the last published version
GET/journeys/:canvasId/versions/migration-previewPreview execution compatibility before a migrate-publish
GET/journeys/:canvasId/versions/:versionIdGet a version
POST/journeys/:canvasId/versions/:versionId/rollbackRoll back to a version
GET/journeys/:canvasId/versions/:versionId/statsVersion-specific stats
GET/journeys/:canvasId/versions/:versionId/executionsExecutions on a specific version
GET/journeys/:canvasId/version-execution-countsExecution counts per version
POST/journeys/:canvasId/versions/:versionId/migrateForce-migrate compatible executions to a version
GET/journeys/:canvasId/historyAudit-log history (limit, max 200)
POST/journeys/bulk-delete / bulk-duplicate / bulk-archive / bulk-unarchiveBulk actions; body { "ids": [...] }, returns { succeeded, failed }
POST/journeys/bulk-tagBulk tagging; body { "ids": [...], "tags": [...] }
POST/journeys/webhook/testFire a webhook node config once against a sample context (rate limited)
POST/journeys/preview-context-valueRender a Liquid expression against a sample scope
POST/journeys/preview-user-updatesDry-run an Update User node's rows against a sample user
POST/journeys/:canvasId/nodes/:nodeId/send-test-email / send-test-sms / send-test-whatsapp / send-test-pushTest-send a message node
POST/journeys/:canvasId/cleanup-stuck-executionsClean up stuck executions
GET/journeys/:canvasId/executions/:executionId/rendered-message/:nodeIdRendered message for one execution + node

Error Responses

All errors share the standard shape - see Error Response in the API Overview for the format, status codes, and rate-limiting behavior.

{
"statusCode": 404,
"message": "Canvas with ID 8f14e45f-ceea-467f-a11d-2f4b6a1c9e3b not found",
"timestamp": "2026-07-12T09:00:00.000Z",
"path": "/journeys/8f14e45f-ceea-467f-a11d-2f4b6a1c9e3b"
}