Retries without doing it twice

A change that fails in transit can be sent again safely: GrowLocal recognises the request by its operationId and answers with the receipt it already has instead of making the change a second time.

Send the same request again

Give every new change a new operationId. If you don't get an answer, or get a 5xx, send the same body again with the same operationId. A change GrowLocal already made is not made again: you get its receipt.

# Run it twice: the second answer is the first receipt, and only one row is added.
curl https://api.growlocal.site/api/v1/profiles/YOUR_BUSINESS_ID/invocations \
  -H "Authorization: Bearer gl_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "target": {
    "featureId": "YOUR_FEATURE_ID",
    "export": "mutateCollection",
    "contractRef": "data.collection.mutate@1"
  },
  "operationId": "quote-request-jordan-1",
  "input": {
    "resourceRef": "YOUR_TABLE_REF",
    "mutation": {
      "kind": "create",
      "values": { "f_name": "Jordan Example", "f_email": "jordan@example.com" }
    }
  }
}'
// Retry the same body, with the same operationId, until an answer arrives.
const body = JSON.stringify({
  target: {
    featureId: "YOUR_FEATURE_ID",
    export: "mutateCollection",
    contractRef: "data.collection.mutate@1",
  },
  operationId: "quote-request-jordan-1",
  input: {
    resourceRef: "YOUR_TABLE_REF",
    mutation: {
      kind: "create",
      values: { f_name: "Jordan Example", f_email: "jordan@example.com" },
    },
  },
});
for (let attempt = 1; attempt <= 3; attempt += 1) {
  try {
    const response = await fetch(
      "https://api.growlocal.site/api/v1/profiles/YOUR_BUSINESS_ID/invocations",
      {
        method: "POST",
        headers: {
          Authorization: "Bearer gl_YOUR_API_KEY",
          "Content-Type": "application/json",
        },
        body,
      },
    );
    if (response.status < 500) {
      console.log(response.status, await response.json());
      break;
    }
  } catch (error) {
    console.log(`attempt ${attempt} failed: ${error}`);
  }
}
import requests

# Retry the same body, with the same operationId, until an answer arrives.
body = {
    "target": {
        "featureId": "YOUR_FEATURE_ID",
        "export": "mutateCollection",
        "contractRef": "data.collection.mutate@1",
    },
    "operationId": "quote-request-jordan-1",
    "input": {
        "resourceRef": "YOUR_TABLE_REF",
        "mutation": {
            "kind": "create",
            "values": {"f_name": "Jordan Example", "f_email": "jordan@example.com"},
        },
    },
}
for attempt in range(1, 4):
    try:
        response = requests.post(
            "https://api.growlocal.site/api/v1/profiles/YOUR_BUSINESS_ID/invocations",
            headers={"Authorization": "Bearer gl_YOUR_API_KEY"},
            json=body,
            timeout=30,
        )
    except requests.RequestException as error:
        print(f"attempt {attempt} failed: {error}")
        continue
    if response.status_code < 500:
        print(response.status_code, response.json())
        break

The same operationId with a different body is refused: a conflict receipt whose diagnostic is DATA.OPERATION_REPLAY_MISMATCH, "this operation identity is already bound to other inputs". An id belongs to one request.

Look a change up

To find out what became of a change without sending it again:

GET /operations/{operationId}

answers that change's receipt, or 404 if this key has made no change with that id. Use it for a pending or uncertain receipt too: look again until the status settles.

curl https://api.growlocal.site/api/v1/profiles/YOUR_BUSINESS_ID/operations/quote-request-jordan-1 \
  -H "Authorization: Bearer gl_YOUR_API_KEY"
const response = await fetch(
  "https://api.growlocal.site/api/v1/profiles/YOUR_BUSINESS_ID/operations/quote-request-jordan-1",
  { headers: { Authorization: "Bearer gl_YOUR_API_KEY" } },
);
console.log(response.status, await response.json());
import requests

response = requests.get(
    "https://api.growlocal.site/api/v1/profiles/YOUR_BUSINESS_ID/operations/quote-request-jordan-1",
    headers={"Authorization": "Bearer gl_YOUR_API_KEY"},
    timeout=30,
)
print(response.status_code, response.json())

Operation ids are kept per key: another key's change with the same id is a different change, and this key can't look it up.

Reads aren't kept

A read changes nothing, so its receipt isn't kept: GET /operations/{operationId} answers 404 for it. Send a read again whenever you need it.

Changing a row someone else changed

A change to an existing row carries the expectedRevision you read it at. If the row has changed since, the answer is a conflict receipt with DATA.REVISION_CONFLICT ("row is at revision …"), and nothing is overwritten. Read the row again, decide, and send the change with its new revision and a new operationId.