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

Create a campaign

POST
/v1/campaigns
curl --request POST \
--url https://api.grona.ai/v1/campaigns \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "name": "Free delivery bar on the home page", "url": "https://shop.example.com/", "externalId": "CMP-1024" }'

Creates a campaign and its variations in one call. A general campaign gets one variation. An abTest campaign gets two, Control and Test, in that order.

  • New campaigns start paused. Nothing reaches visitors until you set the campaign live.
  • The response is the only place that returns an editor link for every variation at once. Each link expires 15 minutes after generatedAt.
  • The snippet check on the campaign’s website runs in the background, so script.status starts as checking.
  • Send externalId to make retries safe: a repeated create returns 409 conflict instead of a second campaign.

trafficSplit is keyed by Control and Test here, because the variation ids do not exist until this call returns. Everywhere else it is keyed by variation id.

Media typeapplication/json

segmentId and segmentValue go together: send both or neither. With neither, every visitor on the targeted pages is eligible. trafficSplit is for abTest campaigns only. slug, condition and list are for multiple campaigns only. Sending any of these with the wrong kind of campaign returns 400.

object
name
required

Campaign name.

string
>= 1 characters
url
required

The page the campaign runs on. With pageTargeting multiple, a sample page of the kind you want to cover; the editor opens this page. It cannot be changed later. Leave out any #fragment: preview reads gronaPreview from the query string only.

string format: uri
externalId

Your own id for a campaign: 1 to 128 letters, digits, _, ., : or -. Unique per workspace. A UUID, an ObjectId or a plain number are all valid.

string
/^[A-Za-z0-9_.:-]{1,128}$/
segmentId

The name of a variable your page sets on window: 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.

string
/^[A-Za-z_][A-Za-z0-9_]{0,127}$/
segmentValue

The value the page variable must hold for a visitor to be eligible: 1 to 128 letters, digits, _, ., : or -. Compared as an exact string match.

string
/^[A-Za-z0-9_.:-]{1,128}$/
type

general creates one variation. abTest creates two, Control and Test.

string
default: general
Allowed values: general abTest
pageTargeting

specific runs on the one page in url. multiple runs on every page matched by slug, condition and list.

string
default: specific
Allowed values: specific multiple
trafficSplit

abTest only. Optional; the default is 50 and 50. Sending it for a general campaign returns 400.

object
Control
required
integer
<= 100
Test
required
integer
<= 100
slug

Required with multiple. One segment of the sample url’s path, spelled exactly as it appears there. On each page, Grona takes the path segment at the same position and compares it with each entry of list. The comparison is exact and case-sensitive. A page with no segment at that position never matches, whichever condition you use.

string
condition

Only for multiple campaigns. Grona takes the path segment of each page at the same position as slug in the sample url. included matches pages whose segment there is in list. not_included matches pages whose segment there is not in list, which can be far more pages than you intend, so preview the variation on some of them before you set the campaign live. A page with no segment at that position never matches.

string
default: included
Allowed values: included not_included
list

Required with multiple. The path segments to compare against, 1 to 50 of them.

Array<string>
>= 1 items <= 50 items

Examples

General campaign on one page

{
"name": "Free delivery bar on the home page",
"url": "https://shop.example.com/",
"externalId": "CMP-1024"
}

Created. The campaign, with an editor link for each variation.

Media typeapplication/json
object
data
required
object
id
required

Grona’s campaign id.

string
externalId
required
One of:

Your own id for a campaign: 1 to 128 letters, digits, _, ., : or -. Unique per workspace. A UUID, an ObjectId or a plain number are all valid.

string
/^[A-Za-z0-9_.:-]{1,128}$/
name
required
string
status
required

live runs the campaign. paused stops it. New campaigns start paused.

string
Allowed values: live paused
domain
required

The website the campaign runs on, resolved from its url.

string | null
script
required

Whether the Grona snippet was found on the campaign’s website.

object
status
required

checking: a check is running. present: the snippet was found. absent: the snippet was not found, or Grona could not load the page. unknown: no check is running and none has completed. New campaigns read checking, not unknown.

string
Allowed values: checking present absent unknown
checkedAt
required
One of:

ISO 8601 in UTC, with millisecond precision.

string format: date-time
createdAt
required

ISO 8601 in UTC, with millisecond precision.

string format: date-time
updatedAt
required

ISO 8601 in UTC, with millisecond precision.

string format: date-time
type
required

general creates one variation. abTest creates two, Control and Test.

string
Allowed values: general abTest
pageTargeting
required

specific runs on the one page in url. multiple runs on every page matched by slug, condition and list.

string
Allowed values: specific multiple
segmentId
required
One of:

The name of a variable your page sets on window: 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.

string
/^[A-Za-z_][A-Za-z0-9_]{0,127}$/
segmentValue
required
One of:

The value the page variable must hold for a visitor to be eligible: 1 to 128 letters, digits, _, ., : or -. Compared as an exact string match.

string
/^[A-Za-z0-9_.:-]{1,128}$/
url

Only for multiple campaigns. The sample page, stored without its query string or fragment.

string format: uri
slug

Only for multiple campaigns. The sample page’s path segment whose position is compared.

string
condition

Only for multiple campaigns. Grona takes the path segment of each page at the same position as slug in the sample url. included matches pages whose segment there is in list. not_included matches pages whose segment there is not in list, which can be far more pages than you intend, so preview the variation on some of them before you set the campaign live. A page with no segment at that position never matches.

string
Allowed values: included not_included
list

Only for multiple campaigns. The segments compared against.

Array<string>
>= 1 items <= 50 items
trafficSplit

Keyed by variation id. On update, every variation must appear and the values must total 100.

object
key
additional properties
integer
<= 100
variations
required

One entry for general, two for abTest in the order Control, Test. Each has an editor link.

Array
object
id
required

The variation id. Use it for editor links and in trafficSplit on update.

string
name
required

Set by Grona. null when the campaign refers to a variation that no longer exists.

string | null
type
required

control when the variation’s name contains the word “control” in any letter case, variant otherwise. An A/B test reads control, variant. A variation whose name is null reads variant.

string
Allowed values: control variant
previewUrl
required

The campaign’s page with the variation’s saved changes applied, through the gronaPreview parameter. It does not expire. null only when the variation no longer exists.

string | null format: uri
editUrl
required

Editor link for this variation. Its token expires 15 minutes after generatedAt.

string format: uri
generatedAt
required

ISO 8601 in UTC, with millisecond precision.

string format: date-time
expiresAt
required

ISO 8601 in UTC, with millisecond precision.

string format: date-time
meta
required
object
workspaceId
required

The workspace resolved from the secret key.

string

Examples

General campaign on one page

{
"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 request is invalid, for example a missing name or url, or a field that does not apply to this kind of campaign. error.code is invalid_request and error.message says what to change. Nothing was created.

Media typeapplication/json
object
error
required
object
code
required

Stable and machine-readable. Branch on this, and on the HTTP status for codes you do not know.

string
Allowed values: invalid_request unauthorized not_found conflict internal_error
message
required

Human-readable. The wording can change, so do not parse it.

string

Example

{
"error": {
"code": "invalid_request",
"message": "`name` is required."
}
}

The secret key is missing, malformed, unknown or revoked. error.code is unauthorized.

Media typeapplication/json
object
error
required
object
code
required

Stable and machine-readable. Branch on this, and on the HTTP status for codes you do not know.

string
Allowed values: invalid_request unauthorized not_found conflict internal_error
message
required

Human-readable. The wording can change, so do not parse it.

string

Example

{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API key."
}
}

The externalId is already used by another campaign in the workspace. error.code is conflict.

Media typeapplication/json
object
error
required
object
code
required

Stable and machine-readable. Branch on this, and on the HTTP status for codes you do not know.

string
Allowed values: invalid_request unauthorized not_found conflict internal_error
message
required

Human-readable. The wording can change, so do not parse it.

string

Example

{
"error": {
"code": "conflict",
"message": "externalId `CMP-1024` is already used by another campaign."
}
}

Too many requests. Retry with exponential backoff and jitter, and wait at least as long as Retry-After when it is present. Branch on the status code; the body is not guaranteed to use the error envelope.

Retry-After
string

How long to wait before retrying, in seconds or as an HTTP date.

Unexpected server error. error.code is internal_error. Retry with backoff; if it persists, contact support@grona.ai with the time of the request and the endpoint.

Media typeapplication/json
object
error
required
object
code
required

Stable and machine-readable. Branch on this, and on the HTTP status for codes you do not know.

string
Allowed values: invalid_request unauthorized not_found conflict internal_error
message
required

Human-readable. The wording can change, so do not parse it.

string

Example

{
"error": {
"code": "internal_error",
"message": "Something went wrong on our side."
}
}

Temporarily unavailable. Retry with exponential backoff and jitter, and wait at least as long as Retry-After when it is present. Branch on the status code; the body is not guaranteed to use the error envelope.

Retry-After
string

How long to wait before retrying, in seconds or as an HTTP date.