Contact cards
A contact card shows what one of your own systems knows about the person you're writing to: their plan, what they owe, when they last signed in, a link into your admin. Each time someone opens a conversation, Jelly asks your system and shows the answer in the conversation sidebar, so nobody has to look the customer up somewhere else.
Card sources
A card source is one of your systems that answers Jelly's questions: your billing system, your CRM, your own admin. A team can have several. Each one gets its own card in the sidebar, in the order the sources were added.
Setting one up takes two people: an Admin, who adds the source in Jelly, and whoever builds the system that answers. The last part of this page is for them.
Adding a card source
Admins and the Owner add card sources.
- Go to Settings → Contact cards and open Add a card source.
- Type a Name, such as Billing. It heads the card when your system's answer has no title of its own.
- Type the URL Jelly should ask. It must start with
https://. - Choose the Time it may take, up to three seconds.
- Press Add card source.
Jelly opens the source's page, which shows its signing secret with a Copy button. Give the secret to whoever builds your system, so it can check that each request really came from Jelly.
To try the source out, type a contact's address under Send a test and press Send a test. Jelly asks the source about them straight away, and shows the card as the sidebar would. If the source doesn't answer with a card, the page tells you why.
You can also add and change card sources with the API.
Changing a card source
On Settings → Contact cards, open a source's menu:
- Edit it opens its page, where you can change its name, URL and time limit, and send a test.
- Turn off stops Jelly asking it, and its card leaves the sidebar. Turn back on brings it back.
- Remove card source…, then Remove the card source, deletes it. There's no undo. If you add the same URL again, it gets a new signing secret.
On a source's page, New signing secret replaces the secret. Your system must use the new one from its next request.
When a card doesn't show
If a source doesn't answer with a card, the sidebar says why, with a Try again button. It'll be one of these:
- the source took longer than its time limit;
- the source answered with an error, or with a redirect;
- the answer was bigger than 64 KB, or wasn't JSON;
- the source's address is on a private network.
Sometimes there's no card and no message either, and that's as it should be. A source that has nothing about this person shows nothing at all. Neither does a conversation with nobody outside the team on it.
If your team leaves Royal Jelly
Your card sources are kept, but Jelly stops asking them, and their cards leave the sidebar. The list shows Not asked. You can still turn a source off or remove it. The cards come back when your team is back on Royal Jelly.
For developers: answering Jelly
This part is for whoever builds the system that answers. Jelly sends
your URL a POST, and you answer with JSON describing the card. Jelly
draws the card itself, so you can't send HTML.
The request
{
"contact": {"id": "jane-customer", "email": "jane@customer.test", "name": "Jane Customer",
"external_id": null, "fields": {}},
"conversation": {"id": "broken-widget-k3f9zq", "subject": "Broken widget",
"external_id": null, "fields": {}},
"requested_by": {"type": "member", "id": 3, "name": "Sarah Chen", "email": "sarah@acme.test",
"role": "admin", "active": true},
"requested_at": "2026-03-04T09:20:00Z"
}
The request never contains anybody's messages or notes: only
addresses, names and ids. external_id and fields are always null
and empty for now. requested_by is the person who opened the
conversation, or the API token that asked.
To answer differently for different people, use the contact's email
or id. To answer for one conversation, use the conversation's id.
A request from Send a test has the conversation id test. Its
contact id is empty when the team has no contact with that address.
Each request carries these headers:
| Header | |
|---|---|
X-Jelly-Signature |
sha256= and the hex HMAC-SHA256 of the request body, using the source's signing secret |
X-Jelly-Timestamp |
Unix seconds, when the request was sent |
X-Jelly-Card-Source |
The card source's id |
Check the signature against the raw request body, before parsing it. This is the same scheme as webhooks.
"sha256=" + OpenSSL::HMAC.hexdigest("SHA256", signing_secret, raw_body)
The body's requested_at is covered by the signature, so you can
refuse a request that's more than a few minutes old.
The answer
If your system has nothing about this person, answer 404. Jelly then
shows nothing at all for your source on that conversation, and asks
again after a minute. Any other error status is shown to the team as a
failure.
Otherwise, answer with a 2xx status and a JSON object:
{
"title": "Acme billing",
"rows": [{"label": "Plan", "value": "Pro", "href": "https://admin.acme.test/c/42"}],
"badges": [{"label": "Past due", "tone": "bad"}],
"links": [{"label": "Open in admin", "href": "https://admin.acme.test/c/42"}],
"ttl_seconds": 300
}
| Field | |
|---|---|
title |
The card's heading. Without one, the card is headed with the source's name |
rows |
Up to 20 pairs of label and value. A value can be text, a number or true/false. A row with an href links its value |
badges |
Up to 6 short labels. tone is ok, warn or bad, or leave it out |
links |
Up to 6 links, each a label and an href |
ttl_seconds |
How long Jelly may show this answer before asking again: 5 to 3600. Without it, one minute |
Every href must start with https://. Jelly leaves out a link that
doesn't, and ignores anything else in the answer that it doesn't
recognise or that's the wrong shape, rather than refusing the whole
card. Text is cut short past 80 characters for the title, 60 for a
label and 300 for a value.
What Jelly holds a source to
- The URL must start with
https://. - The address must be on the public internet. Jelly checks the address when it looks the name up, and again when it connects.
- Jelly doesn't follow redirects.
- The answer must arrive within the source's time limit, three seconds at most, and be 64 KB or less.