Visual editor links
An editor link opens one variation of a campaign in the Grona visual editor, in your user’s browser. Use this page when you add an “Edit” button to your product: it covers how long a link lasts, how to get a fresh one, and how to keep links from leaking.
How a link works
Section titled “How a link works”An editor link looks like this:
https://api.grona.ai/v1/editor/launch?token=eyJhbGciOi...- The token in the query string is what lets the editor open. Anyone who holds a link that has not expired can open the editor for that variation.
- The token expires 15 minutes after it is issued.
generatedAtandexpiresAtin the response are read from the token itself, so they always agree with the link. - The link is opened in a browser. Your backend never calls it.
- Grona does not promise that a link works only once. Treat it as usable by whoever holds it until
expiresAt.
The page you edit must meet the visual editor requirements.
Link lifecycle
Section titled “Link lifecycle”-
Create. Creating a campaign returns an
editUrl,generatedAtandexpiresAtfor every variation. An A/B test returns two links, one for Control and one for Test. -
Open. Send the link to the user’s browser while it is fresh. The user edits the variation and saves.
-
Expire. 15 minutes after
generatedAt, the link stops opening the editor. Changes the user already saved are kept. -
Refresh. Request a new link with
POST /v1/variations/edit-link, or send the link you hold toPOST /v1/variations/edit-link/validateand get back a link that works.
List, get and update do not return editor links. When you need one later, request it with the variation id.
Get a new link
Section titled “Get a new link”Send a variation id from variations[].id on create, list or get:
curl https://api.grona.ai/v1/variations/edit-link \ -H "Authorization: Bearer $GRONA_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"variationId": "671a3c90e4b0a1f2c3d40012"}'// edit-link.mjs (Node 18 or later)const res = await fetch('https://api.grona.ai/v1/variations/edit-link', { method: 'POST', headers: { Authorization: `Bearer ${process.env.GRONA_SECRET_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ variationId: '671a3c90e4b0a1f2c3d40012' }),});const { data } = await res.json();// data.editUrl is good until data.expiresAt.{ "data": { "editUrl": "https://api.grona.ai/v1/editor/launch?token=eyJhbGciOi...", "variationId": "671a3c90e4b0a1f2c3d40012", "previewUrl": "https://shop.example.com/?gronaPreview=671a3c90e4b0a1f2c3d40012", "generatedAt": "2026-10-20T11:12:05.614Z", "expiresAt": "2026-10-20T11:27:05.614Z" }, "meta": { "workspaceId": "65e8a1c47d2f9b03e6a15c42" }}generatedAt and expiresAt describe editUrl only. previewUrl does not expire.
Check a link you hold
Section titled “Check a link you hold”If your product keeps a link for a few minutes, for example while a user decides whether to edit, send it to the validate endpoint before you open it. Send the whole link as Grona issued it, not the token inside it:
curl https://api.grona.ai/v1/variations/edit-link/validate \ -H "Authorization: Bearer $GRONA_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{"editUrl": "https://api.grona.ai/v1/editor/launch?token=eyJhbGciOi..."}'For any Grona editor link whose variation still exists, expired or not, the response is 200 and data.editUrl is the link to open:
- Not expired: the same link comes back byte for byte, with its original
generatedAtandexpiresAt.validistrueandrefreshedisfalse. - Expired: a new link comes back with a full 15-minute lifetime.
validisfalseandrefreshedistrue.
{ "data": { "valid": false, "refreshed": true, "editUrl": "https://api.grona.ai/v1/editor/launch?token=eyJhbGciOi...", "variationId": "671a3c90e4b0a1f2c3d40012", "generatedAt": "2026-10-20T13:41:09.327Z", "expiresAt": "2026-10-20T13:56:09.327Z" }, "meta": { "workspaceId": "65e8a1c47d2f9b03e6a15c42" }}Open data.editUrl either way; you do not need to branch on valid. A returned link that was echoed back may have only seconds left, so open it straight away.
Mint links when the user clicks
Section titled “Mint links when the user clicks”The safest pattern is not to hold links at all. Give your “Edit” button a route on your own backend that checks the user, requests a fresh link and redirects the browser to it. The link then exists for a moment and is never stored.
// server.mjs: an Express route that opens the editor (Node 18 or later).import express from 'express';import { findCampaignForUser } from './campaigns.mjs'; // your own database lookup
const app = express();
app.get('/campaigns/:campaignId/variations/:variationId/edit', async (req, res) => { // 1. Check ownership in your own database. req.user is set by your login middleware. // The campaign must belong to this user, and the variation must belong to the campaign. const campaign = await findCampaignForUser(req.user, req.params.campaignId); if (!campaign || !campaign.gronaVariationIds.includes(req.params.variationId)) { return res.status(404).send('Not found.'); }
// 2. Only then ask Grona for a link to that variation. const grona = await fetch('https://api.grona.ai/v1/variations/edit-link', { method: 'POST', headers: { Authorization: `Bearer ${process.env.GRONA_SECRET_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ variationId: req.params.variationId }), }); if (!grona.ok) return res.status(502).send('Could not open the editor.');
const { data } = await grona.json(); res.set('Cache-Control', 'no-store'); res.redirect(303, data.editUrl);});
app.listen(3000);Your route decides who may edit, so the ownership check in step 1 is not optional. Your secret key covers every campaign in your workspace, including those of all your own customers, and Grona checks only that the variation is in that workspace. It cannot tell your users apart. Without the check, a user who may edit one campaign could put another customer’s variation id in the URL and get a working editor link for it. Never send Grona a variation id taken from a request until you have matched it against the variation ids you saved, at create time, for a campaign the signed-in user owns.
Handle links as secrets
Section titled “Handle links as secrets”Because the token travels in the query string, a link can end up anywhere a URL is recorded.
- Open links straight away. Do not store them in your database, a cookie or local storage.
- Keep them out of your logs, error reports and analytics tools. Redact the
tokenparameter if a URL might be recorded. - Do not send them by email or chat, or put them in a support ticket.
- Do not show them in your UI as text a user could copy and share.
- Send them only to the user who asked to edit, over HTTPS.
Errors
Section titled “Errors”| Status | When | Fix |
|---|---|---|
400 invalid_request |
variationId is missing or not a valid id, or the editUrl sent to validate is not a Grona editor link. |
Send a variation id from a Grona response, or the whole link exactly as issued. |
404 not_found |
The variation does not exist in this workspace, for example because its campaign was deleted. | Read the campaign again for its current variation ids. |
See Errors for authentication errors and retries.
Next steps
Section titled “Next steps”- Verify installations: check that the snippet is on the page before users edit it.
- Create campaigns from your platform: the end-to-end flow these links are part of.