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

Update a campaign

PATCH
/v1/campaigns/{campaignId}
curl --request PATCH \
--url https://api.grona.ai/v1/campaigns/CMP-1024 \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "segmentId": "audience_id", "segmentValue": "returning_buyer", "status": "live" }'

Send only the fields you want to change. At least one field is required. The response is the whole campaign after the change, including its variations.

A campaign’s url, type and pageTargeting are fixed when it is created. The call returns 400 invalid_request and applies nothing when it:

  • sends url, type or pageTargeting;
  • sends slug, condition or list for a specific campaign;
  • sends trafficSplit for a general campaign, or a split that leaves out a variation or does not total 100;
  • sends only one of segmentId and segmentValue for a campaign that has neither.

For a campaign that already has a page variable, sending one of the pair keeps the other. v1 cannot remove page-variable targeting from a campaign; create a new campaign instead.

If you retry an update, address the campaign by Grona’s id, not by externalId. An update can change or clear the externalId, and another campaign can then take it.

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

Grona’s campaign id or your externalId. If a value matches both, Grona’s id wins.

Example

CMP-1024
Media typeapplication/json

At least one field. url, type and pageTargeting cannot be sent.

object
>= 1 properties
name
string
>= 1 characters
status

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

string
Allowed values: live paused
externalId
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}$/
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}$/
trafficSplit

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

object
key
additional properties
integer
<= 100
slug

Only for multiple campaigns. A new segment of the sample url’s path, spelled exactly as it appears there.

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 whole new list, 1 to 50 entries. It replaces the old one.

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

Examples

Add a page variable and set the campaign live

{
"segmentId": "audience_id",
"segmentValue": "returning_buyer",
"status": "live"
}

The whole campaign after the update.

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

In creation order, so Control comes first in an A/B test. No editor links.

Array<object>
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
meta
required
object
workspaceId
required

The workspace resolved from the secret key.

string

Example

{
"data": {
"id": "671a3c90e4b0a1f2c3d40011",
"externalId": "CMP-1024",
"name": "Free delivery bar on the home page",
"status": "live",
"domain": "https://shop.example.com",
"script": {
"status": "present",
"checkedAt": "2026-10-20T09:30:19.884Z"
},
"createdAt": "2026-10-20T09:30:12.418Z",
"updatedAt": "2026-10-20T11:05:37.290Z",
"type": "general",
"pageTargeting": "specific",
"segmentId": "audience_id",
"segmentValue": "returning_buyer",
"variations": [
{
"id": "671a3c90e4b0a1f2c3d40012",
"name": "Free delivery bar on the home page - Variation",
"type": "variant",
"previewUrl": "https://shop.example.com/?gronaPreview=671a3c90e4b0a1f2c3d40012"
}
]
},
"meta": {
"workspaceId": "65e8a1c47d2f9b03e6a15c42"
}
}

The request is invalid, for example it sends a field that is fixed at creation. error.code is invalid_request and error.message says what to change. Nothing in the request was applied.

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": "`url` cannot be changed; create a new campaign for a different page."
}
}

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."
}
}

No campaign with this id or externalId exists in the key’s workspace. error.code is not_found.

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": "not_found",
"message": "Campaign not found."
}
}

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.