Update a campaign
package main
import ( "fmt" "strings" "net/http" "io")
func main() {
url := "https://api.grona.ai/v1/campaigns/CMP-1024"
payload := strings.NewReader("{ \"segmentId\": \"audience_id\", \"segmentValue\": \"returning_buyer\", \"status\": \"live\" }")
req, _ := http.NewRequest("PATCH", url, payload)
req.Header.Add("Authorization", "Bearer <token>") req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close() body, _ := io.ReadAll(res.Body)
fmt.Println(res) fmt.Println(string(body))
}OkHttpClient client = new OkHttpClient();
MediaType mediaType = MediaType.parse("application/json");RequestBody body = RequestBody.create(mediaType, "{ \"segmentId\": \"audience_id\", \"segmentValue\": \"returning_buyer\", \"status\": \"live\" }");Request request = new Request.Builder() .url("https://api.grona.ai/v1/campaigns/CMP-1024") .patch(body) .addHeader("Authorization", "Bearer <token>") .addHeader("Content-Type", "application/json") .build();
Response response = client.newCall(request).execute();const url = 'https://api.grona.ai/v1/campaigns/CMP-1024';const options = { method: 'PATCH', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"segmentId":"audience_id","segmentValue":"returning_buyer","status":"live"}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}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,typeorpageTargeting; - sends
slug,conditionorlistfor aspecificcampaign; - sends
trafficSplitfor ageneralcampaign, or a split that leaves out a variation or does not total 100; - sends only one of
segmentIdandsegmentValuefor 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.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Path Parameters
Section titled “Path Parameters”Grona’s campaign id or your externalId. If a value matches both, Grona’s id wins.
Example
CMP-1024Request Bodyrequired
Section titled “Request Bodyrequired”At least one field. url, type and pageTargeting cannot be sent.
object
live runs the campaign. paused stops it. New campaigns start paused.
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.
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.
Keyed by variation id. On update, every variation must appear and the values must total 100.
object
Only for multiple campaigns. A new segment of the sample url’s path, spelled exactly as it appears there.
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.
Only for multiple campaigns. The whole new list, 1 to 50 entries. It replaces the old one.
Examples
Add a page variable and set the campaign live
{ "segmentId": "audience_id", "segmentValue": "returning_buyer", "status": "live"}Change the split and the page list of a multi-page A/B test
{ "trafficSplit": { "671a3d52e4b0a1f2c3d40022": 50, "671a3d52e4b0a1f2c3d40023": 50 }, "list": [ "dairy", "bakery", "cereal" ]}Responses
Section titled “Responses”The whole campaign after the update.
object
object
Grona’s campaign id.
live runs the campaign. paused stops it. New campaigns start paused.
The website the campaign runs on, resolved from its url.
Whether the Grona snippet was found on the campaign’s website.
object
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.
ISO 8601 in UTC, with millisecond precision.
ISO 8601 in UTC, with millisecond precision.
general creates one variation. abTest creates two, Control and Test.
specific runs on the one page in url. multiple runs on every page matched by
slug, condition and list.
Only for multiple campaigns. The sample page, stored without its query string or fragment.
Only for multiple campaigns. The sample page’s path segment whose position is compared.
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.
Only for multiple campaigns. The segments compared against.
Keyed by variation id. On update, every variation must appear and the values must total 100.
object
In creation order, so Control comes first in an A/B test. No editor links.
object
The variation id. Use it for editor links and in trafficSplit on update.
Set by Grona. null when the campaign refers to a variation that no longer exists.
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.
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.
object
The workspace resolved from the secret key.
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.
object
object
Stable and machine-readable. Branch on this, and on the HTTP status for codes you do not know.
Human-readable. The wording can change, so do not parse it.
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.
object
object
Stable and machine-readable. Branch on this, and on the HTTP status for codes you do not know.
Human-readable. The wording can change, so do not parse it.
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.
object
object
Stable and machine-readable. Branch on this, and on the HTTP status for codes you do not know.
Human-readable. The wording can change, so do not parse it.
Example
{ "error": { "code": "not_found", "message": "Campaign not found." }}The externalId is already used by another campaign in the workspace. error.code is
conflict.
object
object
Stable and machine-readable. Branch on this, and on the HTTP status for codes you do not know.
Human-readable. The wording can change, so do not parse it.
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.
Headers
Section titled “Headers”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.
object
object
Stable and machine-readable. Branch on this, and on the HTTP status for codes you do not know.
Human-readable. The wording can change, so do not parse it.
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.
Headers
Section titled “Headers”How long to wait before retrying, in seconds or as an HTTP date.