Connect an AI assistant (participants)
Let a person’s AI assistant find Jade events and register them. Personal assistants connect with no account and prove the person’s email with an emailed code; partner integrations use an API account. Either way, payment stays with the person, in their browser.
Personal assistants — no account needed
For a person’s own AI assistant — Siri, ChatGPT, Claude, Grok, Muse, or one you build. Any assistant can connect: there is no account, key or OAuth sign-in. Before it can act for someone it proves their email with a code Jade sends them; from then on it works as that person, through the same services as the registration page and the registration dashboard, at the same prices and under the same rules. This is the recommended way to register people from an AI assistant.
submit_registration and submit_changes submit nothing: a client that supports URL elicitation shows the person a Jade payment link, and any other client gets payment_required with a paymentUrl to pass on. Registrations and changes with nothing to pay are submitted by the assistant.Connecting
/api/mcp/agentsstreamable-http transport, statelessly: send each JSON-RPC message as its own POST. No MCP session is kept, so a tools/call works without an initialize first; GET and DELETE answer 405. There is nothing to authenticate: send no Authorization header.
mcpServers file:
{
"mcpServers": {
"jade-events": {
"url": "/api/mcp/agents"
}
}
}mcp-remote bridge:
{
"mcpServers": {
"jade-events": {
"command": "npx",
"args": ["-y", "mcp-remote", "/api/mcp/agents"]
}
}
}application/json and text/event-stream. The answer is one server-sent event carrying the JSON-RPC response, with the tool’s result in both content (as JSON text) and structuredContent. tools/list returns every tool with its input schema.
curl -N /api/mcp/agents \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "search_events",
"arguments": { "query": "sustainability expo chicago" }
}
}'
event: message
data: {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"{\"events\":[...]}"}],"structuredContent":{"events":[...]}}}Signing in
Until the person’s email is proven, an assistant can only search, read a flow’s questions and ask for a code.
Find the event with search_events(words from its name, venue or city, and an optional date range; at most 10 events) orget_event. Each event carries itstenantCode,clientCodeandeventCode, and each of its flows aflowSlugand its public registration page.describe_registrationlists a flow’s questions, so the assistant can gather the answers first.Call start_registrationwith the three codes, the flow, the person’s own email andagentName— the name the person knows the assistant by, up to 40 letters, digits, spaces and plain punctuation. Jade emails a 6-digit code and a Continue in my browser link, saying that an assistant calling itself by that name is registering them. It returns achallengeId; the code expires in 30 minutes. The answer is the same whether the person is new or already registered.- Get the code. An assistant that can read the person’s mailbox finds it in the email from Jade — the subject and the plain-text part both carry it; otherwise ask the person for it. The person can also open the link and carry on in their browser instead.
Call verify_emailwith thechallengeIdand the code. It returnssessionToken,participantId,flowSlug,registrationCompleteand, when the person has already registered,modifyToken. Three wrong codes cancel the code; callstart_registrationagain.
Passing the session
tenantCode, clientCode, eventCode and sessionToken; the ones that work on a finished registration also take modifyToken.
sessionTokenis the token the registration page holds for that flow, valid for 24 hours. Every call checks that it belongs to the event named with it.modifyTokenis the registration dashboard’s token, valid for one hour and renewed as it is used: every tool that uses it returns a freshmodifyToken, so always pass the newest.submit_registrationreturns one once it submits.Both act as the person: keep them out of logs and share them with no one. The registrationUrlandpaymentUrllinks the registration tools return carry the session too, so give them to the person alone.- A Test Mode session, or a +1 guest’s session, is refused.
Tools
cancel_room and cancel_registration only preview — the refund, any fee and the terms — unless called with confirm: true; cancel_registration then also needs an operationKey of your own, sent again unchanged on a retry. Confirm only once the person has agreed to the preview.
| Group | What it covers | Tools |
|---|---|---|
| Find | Search events whose registration accepts personal assistants, read one, and read a flow’s questions. No session needed. | search_eventsget_eventdescribe_registration |
| Sign-in | Email the person a 6-digit code and exchange it for the same registration (and modify) session the registration page uses. | start_registrationverify_email |
| Register | Answer the flow’s questions, choose products at real prices, apply discount codes, accept policies and submit — under the registration page’s own rules. | get_registrationsave_answerslist_productsupdate_cartget_cartapply_discount_codeget_policiesaccept_policysubmit_registration |
| Housing | Search hotels, hold and release rooms, list the booking, join or leave a room waitlist and cancel a booked room. | search_hotelshold_roomsrelease_roomlist_my_roomscancel_roomleave_room_waitlist |
| Change a registration | Read a finished registration, change answers, price and submit changes, or cancel — as the modify dashboard does. | get_my_registrationupdate_registrationpreview_changessubmit_changescancel_registration |
Worked example
Registering Alex for a conference with a paid pass. Calls and results are abridged; every call after verify_email also carries tenantCode, clientCode, eventCode and sessionToken.
// 1. Find the event (no session)
→ search_events { "query": "sustainability expo" }
← { "events": [{
"tenantCode": "acme-group", "clientCode": "acme", "eventCode": "susexpo-2026",
"name": "Sustainability Expo 2026", "startOn": "2026-11-18", "endOn": "2026-11-20",
"timezone": "America/Chicago",
"flows": [{ "flowSlug": "attendee", "name": "Attendee Registration",
"registrationUrl": "/tenant/acme-group/client/acme/event/susexpo-2026/register/attendee" }] }] }
// 2. Email the person a code
→ start_registration { "tenantCode": "acme-group", "clientCode": "acme",
"eventCode": "susexpo-2026", "flowSlug": "attendee",
"email": "alex@example.com", "agentName": "Muse" }
← { "status": "code_sent", "challengeId": "c7f1…", "expiresInMinutes": 30, "note": "…" }
// 3. Exchange the code (read from the inbox, or asked of Alex) for the session
→ verify_email { "challengeId": "c7f1…", "code": "421907" }
← { "sessionToken": "eyJ…", "participantId": "6650…", "flowSlug": "attendee",
"registrationComplete": false, "next": "Use get_registration to see what is still needed." }
// From here on, every call also carries
// "tenantCode", "clientCode", "eventCode" and "sessionToken"
// 4. What is still needed, then the answers
→ get_registration {}
← { "submitted": false,
"questions": [{ "binding": "@participant.firstName", "label": "First Name",
"type": "text", "required": true }, …],
"answers": {}, "progress": { "allPagesComplete": false, "pages": [ … ] },
"registrationUrl": "/tenant/acme-group/client/acme/event/susexpo-2026/register/attendee?token=eyJ…" }
→ save_answers { "answers": { "@participant.firstName": "Alex",
"@participant.lastName": "Rivera", "@participant.company": "Acme" } }
← { "saved": true, "keys": ["@participant.firstName", "@participant.lastName", "@participant.company"] }
// 5. The cart, at the checkout's prices
→ list_products {}
← { "currency": "USD", "products": [{ "productId": "66a0…", "name": "Full Conference Pass", … }] }
→ update_cart { "items": [{ "productId": "66a0…", "quantity": 1 }] }
← { "changes": [ … ], "cart": { … }, "followUps": [] }
→ get_cart {}
← { /* lines, discounts, tax, fees and total — never quote a price it did not return */ }
// 6. Policies: show the person the text, then record their agreement
→ get_policies {}
← { "policies": [{ "policyId": "65a1…", "title": "Code of Conduct",
"required": true, "alreadyAccepted": false, … }],
"outstandingRequired": 1 }
→ accept_policy { "policyIds": ["65a1…"] }
← { "accepted": ["Code of Conduct"], "alreadyAccepted": [] }
// 7. Submit: the pass costs money, so nothing is submitted yet
→ submit_registration {}
← { "status": "payment_required",
"amountDue": { "cents": 49500, "currency": "USD" },
"paymentUrl": "/tenant/acme-group/client/acme/event/susexpo-2026/register/attendee?token=eyJ…",
"note": "Give the person paymentUrl: they review and pay in their browser. Nothing has been submitted." }status: submitted, the orderId when an order was made, and a modifyToken for later changes. With anything required still missing, it returns status: incomplete and a missing list instead.
verify_email also returns a modifyToken: read the registration with get_my_registration, change answers with update_registration, price the change with preview_changes and apply it with submit_changes.
Payment
status: payment_required:
From submit_registration:amountDue(cents and currency) andpaymentUrl— the registration page, with the person’s session, where they review and pay.From submit_changes:amountDueCents,currencyandpaymentUrl— the registration dashboard, where the person signs in with their email, pays, and the change is applied.
URL elicitation: the payment link in the chat
capabilities.elicitation.url, MCP spec 2025-11-25) gets the payment step as a -32042 URL-elicitation error instead of a tool result. The client shows the person the URL and opens it in their browser when they agree. Jade answers the client’s initialize with an Mcp-Session-Id that records the declaration; send it on every later request, as the transport requires.
- For a registration the URL is a Jade page that carries no session. To prove it is the person, it emails a sign-in link to the address they registered with; that link opens their registration, where they review and pay.
- For changes it is the registration dashboard, which signs the person in with their email.
When the person says they are done, call the tool again with the same arguments. submit_registrationthen returnsstatus: submittedwith amodifyToken; if they have not finished, the same kind of elicitation comes back.
hold_rooms are booked when the registration, or the change, is submitted. Holds expire if the person goes quiet; every housing tool call keeps them alive.
Which flows are offered
An assistant sees a flow only when all of these hold:
The organizer turned on Open to personal agents for the flow, in the flow designer under Access. It is off by default. - Its access includes Public or Email-verified: the code proves the address, which is all those ask. Invite-only, passcode, directory, SSO and other gated flows are never offered.
- It is not hidden, not limited to people with certain tags, and has no waiting room. An event’s private guest flows are never offered.
- The event is published and has not ended or been archived, and the flow is open for registration now. Every tool reads the event’s published version.
Anyone can still register for any other flow on the event’s website.
Limits and errors
60 requests a minute per IP address; beyond that, HTTP 429 Too Many Requests.3 codes per email address per event per hour. Past that, start_registrationstill returns an ordinarychallengeId, but no email is sent and no code verifies it — so the endpoint never says whether an address was asked about before.3 wrong codes cancel a code, and its link with it. Failures are tool errors ( isError: true), not HTTP errors, with one answer for each kind, so the endpoint cannot be used to probe events or people. An unknown event, or a flow not open to assistants: “No event open to assistant registration matches that. Use search_events to find one.” A bad, expired or mismatched session: “This session is not valid for that event any more. Start again with start_registration.” A wrong, expired or cancelled code: “That code is incorrect or has expired. Call start_registration for a new one.”- Every other refusal is the registration page’s own — a sold-out product, a closed sales window, a refused discount code, a read-only field — and comes back with its reason.
Being found
personal-agents, in Jade’s MCP discovery document, with authentication: none and every tool it serves:
GET /api/.well-known/mcp.jsonEvent as JSON-LD — name, dates, place and a RegisterAction pointing at the page — and a link to the endpoint. The page’s script adds both as it renders:
<script type="application/ld+json">{ "@type": "Event", ..., "potentialAction": { "@type": "RegisterAction", "target": "…/register/attendee" } }</script>
<link rel="mcp-server" href="/api/mcp/agents">Not supported
- Payment by the assistant. The person pays in their browser.
- Flows with a waiting room: admission is enforced in the browser’s queue, which an assistant never stands in.
- Test Mode, and +1 guests’ sessions.
File uploads and signatures. The person adds them on the registration page, at registrationUrl.- Organizer, tenant-wide and support tools. Organizers connect their own assistant through the organizer endpoint.
Partner integrations (API account)
/mcp/participant is for vetted integrators: an assistant you build or host, with an API account a Jade administrator issues. It reaches the events the account is allowed, and your code sets HTTP headers on every request. The assistant submits what it knows about the person, and the person finishes, and pays, at the link Jade returns.
This part walks through the partner flow: getting an API account, connecting to the endpoint, verifying the person’s email with a one-time code, and submitting a registration that the person completes, paying if needed, at the link Jade returns.
agent_submit_registration saves what the assistant knows about the person and returns a link. When the next step is checkout, the assistant gets that link and never any payment details: the person opens it to pay.Before you start
You need an API account from a Jade administrator, set up under Developers → API Accounts with Allow an AI assistant to register people ticked ( canActAsParticipantAgent: truein the API). It also needs access to the clients or events you will register people for, under Allowed clients or Allowed events: eachallowedClientsentry is a{ tenantCode, clientCode }object and eachallowedEventsentry a{ tenantCode, clientCode, eventCode }object. If the account is also limited to some tenants under Allowed tenants, each event’s tenant must be one of them. Copy the token when it is issued; it is shown once.- You need the tenant, client and event code of each event: the sign-in tools take all three. They are the codes in the account’s allowed clients and events; ask your Jade administrator if you do not have them.
Your MCP client must let your code set HTTP headers on each request: the API token in Authorizationon every call and, once the person is verified, their session token inX-Participant-Session. An assistant you only configure — a custom connector in Claude or ChatGPT, for example — cannot send a different session for each person, so this endpoint is for an assistant you build or host yourself; an assistant you only configure uses the personal-assistant endpoint above.
The server address
Every request goes to the same address:
/api/mcp/participantSend the API token on every request, the two sign-in calls included, and after sign-in the person’s session token as well:
POST /api/mcp/participant
Authorization: Bearer jdo_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
X-Participant-Session: eyJ...session-token...canActAsParticipantAgent gets 403 Forbidden.
participant, with its transport (streamable-http), what it needs to sign in and every tool it serves:
GET /api/.well-known/mcp.jsonSigning in
The assistant proves it acts for the person with a one-time code sent to their email. The two sign-in tools are the only ones that work before a session exists.
1. Request a code
agent_request_otp with the person’s email and the event’s tenantCode, clientCode and eventCode. Jade emails a 6-digit code to that address, and the person reads it back to the assistant. The email opens with your API account’s name, so give the account a name the person will recognise.
// MCP tool call
{
"name": "agent_request_otp",
"arguments": {
"tenantCode": "acme-group",
"clientCode": "acme",
"eventCode": "acme-2026",
"email": "alex@example.com"
}
}
// Response
{
"sent": true,
"expiresInSeconds": 900
}The code expires after 15 minutes. How many codes can be sent is limited; see Rate limits below.
2. Verify the code and keep the session
// MCP tool call
{
"name": "agent_verify_otp",
"arguments": {
"tenantCode": "acme-group",
"clientCode": "acme",
"eventCode": "acme-2026",
"email": "alex@example.com",
"code": "421907"
}
}If the code matches, Jade returns a session token:
{
"verified": true,
"session": {
"token": "eyJ...session-token...",
"expiresAt": "2026-05-21T13:14:00.000Z"
},
"participantExists": false
}X-Participant-Session header on every later call. The session is for that one (tenantCode, clientCode, eventCode, email), works only with your API account, and lasts one hour; after that, request a new code. participantExists says whether the event already had a registration for that email.
verified: false with the reason wrong_code and attemptsRemaining; the third wrong attempt burns the code. While a newer code is still valid, an earlier one counts as a wrong code. The reason expired_or_missing means no unused code matches: it is more than 15 minutes old, has already been used to sign in, was burned by wrong attempts, or was sent for a different event, email or API account. Request a new one.
Using it
With the session token, find the right registration flow, submit what you know about the person, and hand them the link.
1. Find the flows
agent_list_flows to see the event’s registration flows (attendee, exhibitor, speaker and so on) with the fields each one asks for. Use it to choose the flow for the person and to see what you still need to ask them.
// MCP tool call (with X-Participant-Session)
{
"name": "agent_list_flows",
"arguments": {}
}
// Response (abridged)
{
"flows": [
{
"slug": "primary-attendee",
"name": "Attendee Registration",
"visibleForTags": null,
"descriptor": {
"descriptor": {
"eventCode": "acme-2026",
"flowSlug": "primary-attendee",
"fields": [
{
"binding": "@participant.firstName",
"flatKey": "firstName",
"entityPath": "participant.firstName",
"label": "First Name",
"type": "text",
"source": "standard",
"required": true
},
{
"binding": "@custom.tShirtSize",
"flatKey": "tShirtSize",
"entityPath": "customFieldResponses.tShirtSize",
"label": "T-Shirt Size",
"type": "choice",
"source": "custom",
"required": false,
"customFieldCode": "tShirtSize",
"choices": [
{ "code": "M", "name": "Medium" },
{ "code": "L", "name": "Large" }
]
}
]
},
"customFields": [ /* the event's own questions: key, name, responseType, choices */ ],
"timezone": "America/New_York",
"eventStartOn": "2026-10-12"
}
},
{
"slug": "exhibitor",
"name": "Exhibitor Registration",
// Shown only to people with the exhibitor tag
"visibleForTags": { "type": "all", "required": ["exhibitor"] },
// Same shape as above; null if the flow could not be described
"descriptor": { /* ... */ }
}
]
}2. Submit the registration
flowSlug and the event’s first visible flow is used.
// MCP tool call (with X-Participant-Session)
{
"name": "agent_submit_registration",
"arguments": {
"flowSlug": "primary-attendee",
"data": {
"@participant.firstName": "Alex",
"@participant.lastName": "Rivera",
"@custom.tShirtSize": "L",
"@custom.dietaryNotes": "vegetarian"
}
}
}
// Response (new registration)
{
"status": "new",
"matchStatus": "new",
"url": "/tenant/acme-group/client/acme/event/acme-2026/register/primary-attendee?token=eyJ...registration-token...",
"jwt": "eyJ...registration-token...",
"participantId": "507f1f77bcf86cd799439011",
"flowSlug": "primary-attendee"
}@entity.property token format: @participant.* for built-in fields and @custom.* for the event’s own questions. The session’s verified email is always saved as @participant.email, so a session cannot register a different address.
3. Hand off to the person
The response carries:
status(andmatchStatus, the same value):newfor a new registration,existing_incompletewhen the person had already started one, which the link continues, orexisting_completewhen they are already registered.url— the link the person opens to finish, payment included. Forexisting_completeit is instead the sign-in page for changing a registration, with their email filled in.jwt— the registration token already in the link, valid for 24 hours; submit again for a fresh link. It is not returned forexisting_complete.flowSlug— the flow used (not forexisting_complete) — and, for a new registration only,participantId.When a field fails validation the response is instead status: validation_failedwith anerrorslist, and nothing is saved. Fix the fields and submit again.
Give the person the link. It opens their registration without another sign-in, so send it to them alone; the rest of the flow, payment included, happens in their browser.
4. After they finish
get_my_registration, check_balance_due and describe_my_flow to confirm the registration or fetch its details. They act on the person’s own registration, so they need a session issued once it exists: if agent_verify_otp returned participantExists: false, they answer “No verified participant identity for this run.” Request and verify a new code after agent_submit_registration, and use the new session.
recommend_products, recommend_hotel, recommend_companies and recommend_participants — and generate_participant_profile. They work from the registered person’s details, so with a session issued before the registration existed they answer “No participant identity available”.
Tools
agent_request_otp and agent_verify_otp needs the session token, and most of the rest act on the person’s own registration, so they need a session issued after it exists (see After they finish).
| Group | What it covers | Tools |
|---|---|---|
| Sign-in | Email the person a one-time code and exchange it for their session. The only tools that work before a session exists. | agent_request_otpagent_verify_otp |
| Registration | List the flows, submit a registration, then read and update the person’s own registration, their balance due and their confirmation email. | agent_list_flowsagent_submit_registrationdescribe_my_flowget_my_registrationupdate_my_registrationcheck_balance_dueresend_my_confirmation_emaillist_data_fields |
| Housing | The event’s hotels, and a hotel recommendation for the person. | get_hotel_listrecommend_hotel |
| Groups | Register the members of the person’s group from a template. | get_group_import_templateimport_group_members |
| Exhibitors & appointments | The booth map, exhibitor availability, and booking or cancelling the person’s appointments. | get_booth_maplist_exhibitor_availabilitybook_exhibitor_appointmentlist_my_appointmentscancel_my_appointment |
| Recommendations | Products, exhibiting companies and other participants suited to the person, from their profile, once the session knows the registered person. | recommend_productsrecommend_companiesrecommend_participantsgenerate_participant_profile |
What is not available, and why
The endpoint acts for one verified person at one event, so it serves only what that person may do for themselves. Left out:
- Working through the form with the person: the cart, the product list, policy agreements, moving between form pages and form progress. Jade’s own in-registration assistant has these because it sits beside the form the person has open; here the person does those steps in their browser, at the link.
Payment. The assistant never sees payment details; the person pays at the link agent_submit_registrationreturns.- Organizer tools — event setup, other people’s registrations, reports and exports. They need an organizer’s own Jade login, through the organizer endpoint.
- Tenant-wide tools — queries across events, creating events and account administration.
- Jade’s support-inbox tools, and the tools an exhibiting company’s administrators use for their company.
Actions that cannot be undone
The assistant acts as the person, and some tools send email or change what Jade holds about them. Once one has run, the assistant cannot take it back:
agent_request_otpemails a sign-in code to the address it is given.agent_submit_registrationcreates a registration for the person from the data it is sent. When the event already has one with the same name and email, it saves nothing and links to that one instead.update_my_registrationchanges the person’s saved answers.import_group_membersregisters someone else as a member of the person’s group, with a hotel reservation when a stay is sent in plain keys such ashotelId.book_exhibitor_appointmentbooks the appointment and shares the person’s details with the exhibitor as a lead. Jade emails the person a confirmation and tells the exhibitor’s contact.cancel_my_appointmentcancels the appointment and emails the other side — the other participant, or the exhibitor’s contact. A cancelled booth slot is free for anyone to book.resend_my_confirmation_emailsends one of the person’s emails to them again. Called without anemailId, it only lists the ones it can resend.generate_participant_profilewrites a short AI-generated description of the person and saves it on their record, replacing any earlier one.
agent_submit_registration returns. Before each one, the assistant should tell the person what it is about to do — the address a code goes to, the answers it will save, the member it will add, the appointment it will book or cancel, the email it will resend — and go ahead only once they agree.
Revoking access
Sessions also end on their own after one hour.
Rate limits
agent_request_otp returns a tool error — “OTP rate limit exceeded for this email — try again in an hour” or “Daily OTP quota exceeded for this API account” — rather than an HTTP 429. Jade sets no per-minute request limit on this endpoint; the API account’s rateLimitPerMinute applies to the REST API only.
Troubleshooting
- 401 “Missing Authorization header” or “Invalid or revoked API token”
- Send Authorization: Bearer and the token on every request, the two sign-in calls included. A revoked or expired token, or one whose API account has been deactivated, fails at once; ask your Jade administrator for a new one.
- 403 “API account is not enabled for the participant MCP endpoint”
- The account does not have Allow an AI assistant to register people ticked. Ask your Jade administrator to tick it under Developers → API Accounts.
- “API account is not authorized for …”
- The tenant, client and event codes are not covered by the account’s allowed tenants, clients or events. Check the codes, or ask your Jade administrator to add the event.
- “Event … does not belong to client …”
- The event is under a different client. Check the client code you passed with it.
- agent_verify_otp returns verified: false
- With wrong_code, attemptsRemaining says how many tries are left; while a newer code is still valid, an earlier one counts as wrong. With expired_or_missing, no unused code matches: it is more than 15 minutes old, has already been used to sign in, was burned by 3 wrong attempts, or was sent for a different event, email or API account. Request a new one.
- “Missing X-Participant-Session header — call agent_verify_otp first”
- Every tool except the two sign-in tools needs the session token from agent_verify_otp, sent in the X-Participant-Session header.
- “Invalid or expired participant session”
- A session lasts one hour and works only with the API account that verified it. Request and verify a new code.
- “This API account is no longer authorized for this event.”
- The account’s access to that event was removed after the session was issued; every call checks it again. Ask your Jade administrator.
- “No verified participant identity for this run.” or “No participant identity available” just after you registered someone
- The session was issued before the person’s registration existed (participantExists: false). Request and verify a new code, then use the new session.
- agent_submit_registration returns status: validation_failed
- errors lists what to fix, and nothing was saved. Check the fields against the flow’s descriptor from agent_list_flows, then submit again.
- “OTP rate limit exceeded for this email …” or “Daily OTP quota exceeded for this API account”
- A limit on sign-in codes was reached (see Rate limits). Wait: the per-email limit counts the codes sent to that address in the last hour, and the per-account limit the codes the account sent in the last 24 hours.