Skip to content
The Public API is available on request. Email support@grona.ai to get access.

Create campaigns from your platform

This guide takes one campaign from your product to live visitors on your website, using the Public API from your backend. Follow it when you want users of your own platform to create Grona campaigns without opening the Grona dashboard. The example is Acme Market’s “Free delivery bar on the home page” campaign; the options at the end show an A/B test across several pages.

  • You need a workspace secret key in the GRONA_SECRET_KEY environment variable of the server that calls Grona. See Authentication.
  • The Grona snippet must be installed on your website. See Install the Grona script.
  • The Node samples need Node 18 or later and use this small helper. Save it next to the other files as grona.mjs:
// grona.mjs: a small client for the Grona Public API (Node 18 or later).
export async function grona(method, path, body) {
const res = await fetch(`https://api.grona.ai${path}`, {
method,
headers: {
Authorization: `Bearer ${process.env.GRONA_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: body === undefined ? undefined : JSON.stringify(body),
});
const json = await res.json().catch(() => null);
if (!res.ok) {
throw new Error(`Grona ${res.status} ${json?.error?.code ?? ''}: ${json?.error?.message ?? ''}`);
}
return json;
}
  1. Check that the snippet is on the page.

    Campaigns reach visitors through the Grona snippet, so confirm it is on the page the campaign will run on before you create anything.

    Terminal window
    curl https://api.grona.ai/v1/websites/verify-script \
    -H "Authorization: Bearer $GRONA_SECRET_KEY" \
    -H "Content-Type: application/json" \
    -d '{"url": "https://shop.example.com/"}'

    present means the snippet was found. absent means it was not found or the page could not be loaded. The check covers only the page you send. If you get absent on a page where the snippet is installed, see Verify installations.

  2. Create the campaign.

    Send a name and the page. externalId stores the id of the record in your own product and makes retries safe. This campaign shows to every visitor on the page; Campaign options covers A/B tests, several pages and page variables.

    Everything the browser needs to run a campaign is in your site’s public script, where anyone can read it: the campaign name, the pages it runs on and any page variable. Do not put customer names or other private data in them.

    Terminal window
    curl https://api.grona.ai/v1/campaigns \
    -H "Authorization: Bearer $GRONA_SECRET_KEY" \
    -H "Content-Type: application/json" \
    -d '{
    "name": "Free delivery bar on the home page",
    "url": "https://shop.example.com/",
    "externalId": "CMP-1024"
    }'

    The response is 201 Created:

    {
    "data": {
    "id": "671a3c90e4b0a1f2c3d40011",
    "externalId": "CMP-1024",
    "name": "Free delivery bar on the home page",
    "status": "paused",
    "domain": "https://shop.example.com",
    "script": { "status": "checking", "checkedAt": null },
    "createdAt": "2026-10-20T09:30:12.418Z",
    "updatedAt": "2026-10-20T09:30:12.702Z",
    "type": "general",
    "pageTargeting": "specific",
    "segmentId": null,
    "segmentValue": null,
    "variations": [
    {
    "id": "671a3c90e4b0a1f2c3d40012",
    "name": "Free delivery bar on the home page - Variation",
    "type": "variant",
    "previewUrl": "https://shop.example.com/?gronaPreview=671a3c90e4b0a1f2c3d40012",
    "editUrl": "https://api.grona.ai/v1/editor/launch?token=eyJhbGciOi...",
    "generatedAt": "2026-10-20T09:30:12.731Z",
    "expiresAt": "2026-10-20T09:45:12.731Z"
    }
    ]
    },
    "meta": { "workspaceId": "65e8a1c47d2f9b03e6a15c42" }
    }
    • The campaign starts paused. Nothing reaches visitors until step 5.
    • Save data.id and every variations[].id. You need the variation ids for editor links and, in an A/B test, for the traffic split.
    • script.status is checking because Grona checks the campaign’s website in the background after the call returns.
    • Create is the only call that returns editor links for every variation, and each one expires 15 minutes after generatedAt.
  3. Open the editor links.

    Each variation has an editUrl that opens it in the Grona visual editor. Send it to your user’s browser, for example as a redirect from your “Edit” button. The user makes their changes in the editor and saves them. A general campaign has one link. An A/B test has two, one for Control and one for Test, and authoring the test means opening each of them.

    If the link has expired, request a new one with the variation id:

    Terminal window
    curl https://api.grona.ai/v1/variations/edit-link \
    -H "Authorization: Bearer $GRONA_SECRET_KEY" \
    -H "Content-Type: application/json" \
    -d '{"variationId": "671a3c90e4b0a1f2c3d40012"}'
  4. Preview the variation.

    Open the variation’s previewUrl to see the page with the saved changes applied. It is the campaign’s page with gronaPreview added, using & instead of ? when the URL already has a query string:

    https://shop.example.com/?gronaPreview=671a3c90e4b0a1f2c3d40012
    https://shop.example.com/search?q=milk&gronaPreview=671a3c90e4b0a1f2c3d40012

    Preview works while the campaign is paused. It ignores the campaign’s targeting and consent settings, and it records no page views or conversions. The parameter must stay in the URL: preview does not follow the visitor to the next page. A preview link does not expire and is not private, so anyone who has it can see the variation. See Preview and QA a variation.

    Grona reads gronaPreview from the query string only, so give a campaign a url without a #fragment.

  5. Set the campaign live.

    When the variation is ready, set the status to live:

    Terminal window
    curl -X PATCH https://api.grona.ai/v1/campaigns/CMP-1024/status \
    -H "Authorization: Bearer $GRONA_SECRET_KEY" \
    -H "Content-Type: application/json" \
    -d '{"status": "live"}'

    The response is { "data": { "status": "live" }, "meta": { ... } }. Grona then rebuilds your site script, and visitors see the campaign once the refreshed script reaches them, so the change is not instant. Pausing works the same way with "paused", and has the same delay.

  6. Track conversions and read the results.

    Grona measures the campaign against the conversion goals set up for your workspace. To count a conversion that happens in your own code, such as a completed order, call grona('track', ...) on the page. See Track conversions and custom events.

    The Public API has no endpoint for results. Read them in the Grona dashboard.

The create call accepts three options beyond a name and a page. You can combine them.

Set type to abTest and Grona creates two variations, Control and Test, in that order. trafficSplit sets the share of eligible visitors each one gets, as whole numbers that total 100. Leave it out to split 50 and 50.

On create, the split is keyed by Control and Test, because the variation ids do not exist yet. Every response, and every update, keys it by variation id instead. Conventions has both shapes.

Set pageTargeting to multiple to run one campaign on a set of pages instead of one page. Four fields describe the set:

Field Meaning
url A sample page of the kind you want to cover. The editor opens this page. Grona stores it without its query string or fragment.
slug One segment of the sample page’s path, spelled exactly as it appears there. What counts is its position in the path. Required.
condition included (the default) matches pages whose segment at that position is in list. not_included matches pages whose segment at that position is not in list.
list The segments to compare against, 1 to 50 of them. Required.

On each page, Grona takes the path segment that sits at the same position as slug in the sample url, and compares it with each entry of list. The comparison is exact and case-sensitive. A page that has no segment at that position never matches, whichever condition you use.

This request creates an A/B test on every dairy and bakery page, with 60% of eligible visitors in Control. It has no page variable, so every visitor on those pages is eligible:

{
"name": "Breakfast banner on dairy and bakery pages",
"url": "https://shop.example.com/dairy/whole-milk",
"type": "abTest",
"pageTargeting": "multiple",
"slug": "dairy",
"condition": "included",
"list": ["dairy", "bakery"],
"trafficSplit": { "Control": 60, "Test": 40 }
}

dairy is the first segment of the sample path, so Grona compares the first segment of each page:

  • https://shop.example.com/bakery/sourdough matches: bakery is in the list.
  • https://shop.example.com/frozen/peas does not match: frozen is not in the list.
  • https://shop.example.com/Dairy/whole-milk does not match: Dairy is not dairy.
  • https://shop.example.com/ never matches: it has no first segment.

Sending slug, condition or list without multiple returns 400.

A campaign’s url, type and pageTargeting are fixed once it is created. You can change slug, condition and list later with an update; a new list replaces the old one.

segmentId and segmentValue limit a campaign to visitors whose page sets a variable to a given value. segmentId is the variable’s name and segmentValue is the value it must hold. Send both or neither. With neither, every visitor on the targeted pages is eligible.

Your page sets the variable on window:

<script>
window.audience_id = 'returning_buyer';
</script>

The create request names the variable and the value:

{
"name": "Free delivery bar for returning buyers",
"url": "https://shop.example.com/",
"segmentId": "audience_id",
"segmentValue": "returning_buyer"
}
  • Name: letters, digits and _, not starting with a digit, at most 128 characters. Names the browser already defines, such as name, location, status, top, parent and history, are rejected.
  • Value: 1 to 128 letters, digits, _, ., : or -.
  • Match: the variable must equal the value exactly, as a string.

Target visitors by page variable covers how to set the variable on your site, and Page variables is the reference for the fields.

  • Change a campaign with PATCH /v1/campaigns/{campaignId}. Send only the fields to change; the response is the whole campaign. See Update a campaign.
  • Read campaigns with GET /v1/campaigns or GET /v1/campaigns/{campaignId}, using Grona’s id or your externalId.
  • Delete a campaign with DELETE /v1/campaigns/{campaignId}. Deletion is permanent and frees the externalId. Like a status change, it reaches visitors once your site script refreshes. If you retry an update or a delete, use Grona’s id, not your externalId; see Conventions.