Skip to content

Widget SDK reference

The full reference for window.gro, the SDK your widget uses to talk to the customer portal. New to widgets? Start with How to create widgets.

Every method that changes something returns a Promise. It rejects with an Error whose .code (and .message) is one of the error codes.

Runs callback once the portal has sent your widget its context. Do all your work in here. If the context has already arrived, callback runs straight away.

window.gro.onReady(function () {
// gro.context, gro.branding and gro.scopes are ready here
});

Runs callback(context) every time the context changes after the first load: after a trigger finishes, after the customer uses a portal button, or when the portal refreshes the subscription. Returns a function that stops listening.

const stop = window.gro.onContextChange(function (context) {
render(context);
});
// later: stop();

Runs callback(branding) when the store’s theme changes after the first load, for example while the merchant edits it in the dashboard’s live preview. The theme CSS variables are already updated by then, so you only need this if your script reads theme values. Returns a function that stops listening.

const stop = window.gro.onBrandingChange(function (branding) {
redraw(branding.theme);
});

Re-measures your content and tells the portal how tall to make the frame. Call it after every render.

Read only. What the portal already knows about the current subscription. It never contains the session token, payment details, or the customer’s contact details.

Field Type Notes
subscription.id string
subscription.status string ACTIVE, PAUSED, CANCELLED, …
subscription.order_count number | null Orders actually received
subscription.next_billing_date string | null ISO date
subscription.next_delivery_date string | null ISO date. Null when delivery isn’t tracked
triggers string[] Flows this widget can open right now. See triggers
entities object[] { id, name, entity_type, attributes } for each entity
lines object[] { id, product_title, variant_title } for each line

The store’s portal branding, so your widget can match it.

Field Type
primary_color string (CSS color)
font_family string
border_radius string (CSS length), the button radius
logo_url string
theme object, every value of the store’s portal theme

Any field can be missing.

Every theme value is also a CSS variable in your widget (--gro-card-radius, --gro-brand-color and so on). See Styling your widgets for the full list of values, their defaults and examples.

The scopes your widget was granted, e.g. ["entity.update", "flow.skip"].

true in portal preview. Nothing can change in preview: every action and trigger rejects with USER_CANCELLED.

Triggers open the portal’s own flows. They never change anything themselves; the customer completes the flow in the portal’s UI, with your reasons, playbooks and date rules. See Hooking into portal actions for a walkthrough.

Method Scope Flow name Available when
gro.trigger.skip() flow.skip skip Active and skipping allowed
gro.trigger.sendNow() flow.ship_now ship_now Active, rescheduling allowed, no order already on its way, not retrying a failed payment
gro.trigger.reschedule() flow.reschedule reschedule Active and rescheduling allowed
gro.trigger.pause() flow.pause pause Active and pausing allowed
gro.trigger.resume() flow.resume resume Paused and resuming allowed

Each resolves when the customer closes the flow:

const result = await window.gro.trigger.skip();
// { flow: "skip", outcome: "completed" | "saved" | "dismissed" }
outcome Meaning
completed They did it. For reschedule, choosing “Send ASAP” also counts
saved They accepted an offer from your retention playbook instead
dismissed They closed the flow without doing anything

Closing the flow resolves with dismissed; it doesn’t reject. A trigger only rejects when it can’t open: SCOPE_DENIED, NOT_AVAILABLE, FLOW_BUSY, NEEDS_CLICK, RATE_LIMITED, or USER_CANCELLED in preview.

Call triggers from a click or key press handler. Without a recent click by the customer they reject with NEEDS_CLICK.

true if the trigger can run right now. Takes the method name or the flow name:

window.gro.trigger.can("sendNow"); // same as can("ship_now")

It reads gro.context.triggers, which lists only flows you have the scope for and that are available for the subscription’s current state.

Returns the entity with this id from gro.context, or null. No network call.

Scope: entity.update. No confirm prompt.

changes field Type Notes
name string Optional
attributes object Replaces all attributes. Flat values only (see below)
image_url string | null Optional

Attribute values must be a string, number, boolean or a list of strings. Because attributes replaces the whole set, copy the existing ones first:

const entity = window.gro.entity.get(id);
await window.gro.entity.update(id, {
attributes: Object.assign({}, entity.attributes, { weight: 13.1 }),
});

Scope: subscription.activity_log. No confirm prompt. Write only.

entry field Type Notes
action string Always stored with a widget_ prefix
description string Shown on the activity feed
changes { [field]: { from, to } } Shown inline as was/now
metadata object Free form. Image URLs show as thumbnails

Scope: media.upload. No confirm prompt (the customer already picked the file). Write only.

Takes a File and resolves with { data: { url } }, a public URL for the stored image. PNG, JPEG, WEBP and GIF only, up to 5 MB, checked by the file’s real bytes.

Scope: link.open. Not a method: it lets normal links open a new tab.

<a href="https://example.com/review" target="_blank" rel="noopener"
>Leave a review</a
>

Browsers still need a real click. The new tab opens outside the sandbox, so the site works normally.

Grant only what your widget uses.

Scope Unlocks Customer sees
entity.update gro.entity.update() Nothing extra
subscription.activity_log gro.activity.log() Nothing extra
media.upload gro.media.upload() Their own file picker
link.open Links in a new tab A new tab after clicking
flow.skip gro.trigger.skip() The portal’s skip flow
flow.ship_now gro.trigger.sendNow() The portal’s send now flow
flow.reschedule gro.trigger.reschedule() The portal’s reschedule flow
flow.pause gro.trigger.pause() The portal’s pause flow
flow.resume gro.trigger.resume() The portal’s resume flow
Code When
SCOPE_DENIED The widget wasn’t granted the scope for this call
SETTING_DISABLED The store has turned this off in portal settings
VALIDATION_FAILED Bad input, e.g. a missing entity id or file
NOT_AVAILABLE The trigger can’t run for this subscription right now
FLOW_BUSY Another portal flow is already open
NEEDS_CLICK A trigger was called without a recent click or key press
RATE_LIMITED Too many calls. Bursts of 5 are fine, then 1 per second
USER_CANCELLED The customer declined a confirm prompt, or you’re in preview
BACKEND_ERROR The server rejected the change. Try again or show a friendly message
UNKNOWN_CAPABILITY The call isn’t supported by this version of the portal

Your bundle runs in a sandboxed iframe with no origin and a strict Content Security Policy:

  • No fetch, XMLHttpRequest, WebSockets or beacons (connect-src 'none').
  • No access to the portal’s cookies, storage, page or session token.
  • Scripts and styles must be inline, so bundle everything into your one file. External scripts and stylesheets won’t load.
  • Web fonts load from Google Fonts only (fonts.googleapis.com and fonts.gstatic.com). The store’s theme font is loaded for you.
  • Images work from any https: URL or a data: URL.
  • Render into #gro-widget-root.