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

Errors

When a call fails, the Public API returns an HTTP status and, for most failures, an error body with a stable code. This page lists every code, the usual causes and the fix, and explains how to retry the failures that are temporary.

Apart from 429 and 503, described below, errors use one shape on every Public API endpoint:

{
"error": {
"code": "invalid_request",
"message": "`url` cannot be changed; create a new campaign for a different page."
}
}
  • code is stable and machine-readable. Branch on it.
  • message is for people. Its wording can change, so log it but do not parse it.
  • Treat a code you do not recognise by its HTTP status. New codes can be added within v1.

A request that fails with 400 changes nothing, even when only one of its fields was at fault.

HTTP error.code Meaning How to fix
400 invalid_request The request is malformed or breaks a rule, for example a missing field, a bad format or a field that does not apply to this campaign. Read message, correct the request and send it again. See the common causes below.
401 unauthorized The secret key is missing, malformed, unknown or revoked. Check the header and the key. See Authentication. Do not retry until the key changes.
404 not_found No campaign or variation with this id exists in the key’s workspace. Check the id, or the externalId you looked up. An id from another workspace also returns 404.
409 conflict The externalId is already used by another campaign in the workspace. Use a different externalId, or, if you were retrying a create, fetch the existing campaign. See Conventions.
429 none guaranteed Too many requests. Retry with exponential backoff and jitter, and honour Retry-After.
500 internal_error Something failed on Grona’s side. Retry with backoff. If it persists, contact support.
503 none guaranteed Grona is temporarily unavailable. Retry with exponential backoff and jitter, and honour Retry-After.

A 429 or 503 body is not guaranteed to use the error envelope, so decide on the status code alone.

Cause Fix
name or url missing on create. Send both.
segmentId is not a valid name: it starts with a digit, has characters other than letters, digits and _, is longer than 128 characters, or is a name the browser already defines such as name, location, status, top, parent or history. Rename the page variable, for example to audience_id.
segmentValue has characters other than letters, digits, _, ., : and -, or is longer than 128 characters. Use a value such as returning_buyer.
Only one of segmentId and segmentValue sent on create, or on an update to a campaign that has neither. Send both, or neither.
trafficSplit sent for a general campaign. Leave it out, or create the campaign with type: "abTest".
trafficSplit values do not total 100, or an update leaves out one of the variations. Send whole numbers for every variation that total 100.
slug, condition or list sent without pageTargeting: "multiple", or multiple sent without slug or list. Send the three together with multiple, or leave them all out.
list is empty or has more than 50 entries. Send 1 to 50 entries.
Update sends url, type or pageTargeting. These are fixed at creation. Create a new campaign instead.
Update has an empty body. Send at least one field.
status is not live or paused. Send one of the two.
The campaign id in the path, or a variationId, is not a valid id. Use an id from a Grona response or an externalId you set.
The url sent to verify-script is not an http or https URL. Send a full page URL.
The editUrl sent to validate is not a Grona editor link. Send the whole link exactly as Grona issued it.
The customerId sent to mobile grants is missing, or is not 1 to 128 letters, digits, ., _, : or -. Send the opaque customer id from the shopper’s session.

429, 500 and 503 can succeed on a later attempt. Network timeouts can too. Retry them with exponential backoff and full jitter, and when the response has a Retry-After header, wait at least that long. It holds either a number of seconds or an HTTP date. Grona publishes no fixed rate limits, so do not hard-code a request rate; let 429 responses slow you down.

Not every request is safe to repeat blindly:

Request Safe to retry?
GET list and get Yes.
PATCH update and set status Yes, by Grona’s campaign id. Sending the same fields again gives the same result. Do not retry by externalId: an update can change or clear it, and another campaign can then take it.
DELETE Yes, by Grona’s campaign id. If the first attempt succeeded, the retry returns 404; treat that as done. Never retry a delete by externalId: once the first attempt frees it, a retry can delete a new campaign that took it.
POST /v1/campaigns Only with an externalId. A retry of a create that already succeeded then returns 409, and you fetch the campaign by its externalId. Without one, a retry can create a duplicate.
POST edit-link, validate and verify-script Yes. Each call returns a fresh answer.
POST /v1/mobile/grants Yes. Repeat calls for the same customer return that customer’s currently valid grants. Grona does not mint a new grant for a campaign while an unexpired one exists for that customer, so a retry cannot multiply the grants a shopper can redeem.

Do not retry 400, 401, 404 or 409 unchanged. They fail the same way until you change the request.

// retry.mjs: a fetch wrapper for the Grona Public API (Node 18 or later).
const RETRYABLE = new Set([429, 500, 503]);
function retryAfterMs(res) {
const value = res.headers.get('retry-after');
if (!value) return 0;
const seconds = Number(value);
if (!Number.isNaN(seconds)) return seconds * 1000;
return Math.max(0, Date.parse(value) - Date.now());
}
export async function gronaFetch(path, init = {}, maxAttempts = 5) {
for (let attempt = 1; ; attempt++) {
const res = await fetch(`https://api.grona.ai${path}`, {
...init,
headers: {
Authorization: `Bearer ${process.env.GRONA_SECRET_KEY}`,
'Content-Type': 'application/json',
...init.headers,
},
});
if (!RETRYABLE.has(res.status) || attempt === maxAttempts) return res;
const backoff = Math.random() * Math.min(30_000, 500 * 2 ** attempt); // full jitter
await new Promise((r) => setTimeout(r, Math.max(backoff, retryAfterMs(res))));
}
}

If an error persists or does not match this page, email support@grona.ai with the time of the request in UTC, the method and path, the HTTP status and the error.code. Never include your secret key.