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.

FieldTypeNotes
subscription.idstring
subscription.statusstringACTIVE, PAUSED, CANCELLED, …
subscription.order_countnumber | nullOrders actually received
subscription.next_billing_datestring | nullISO date
subscription.next_delivery_datestring | nullISO date. Null when delivery isn’t tracked
triggersstring[]Flows this widget can open right now. See triggers
entitiesobject[]{ id, name, entity_type, attributes } for each entity
linesobject[]{ id, product_title, variant_title } for each line

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

FieldType
primary_colorstring (CSS color)
font_familystring
border_radiusstring (CSS length), the button radius
logo_urlstring
themeobject, 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.

MethodScopeFlow nameAvailable when
gro.trigger.skip()flow.skipskipActive and skipping allowed
gro.trigger.sendNow()flow.ship_nowship_nowActive, rescheduling allowed, no order already on its way, not retrying a failed payment
gro.trigger.reschedule()flow.reschedulerescheduleActive and rescheduling allowed
gro.trigger.pause()flow.pausepauseActive and pausing allowed
gro.trigger.resume()flow.resumeresumePaused and resuming allowed

Each resolves when the customer closes the flow:

const result = await window.gro.trigger.skip();
// { flow: "skip", outcome: "completed" | "saved" | "dismissed" }
outcomeMeaning
completedThey did it. For reschedule, choosing “Send ASAP” also counts
savedThey accepted an offer from your retention playbook instead
dismissedThey 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 fieldTypeNotes
namestringOptional
attributesobjectReplaces all attributes. Flat values only (see below)
image_urlstring | nullOptional

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 fieldTypeNotes
actionstringAlways stored with a widget_ prefix
descriptionstringShown on the activity feed
changes{ [field]: { from, to } }Shown inline as was/now
metadataobjectFree 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.

ScopeUnlocksCustomer sees
entity.updategro.entity.update()Nothing extra
subscription.activity_loggro.activity.log()Nothing extra
media.uploadgro.media.upload()Their own file picker
link.openLinks in a new tabA new tab after clicking
flow.skipgro.trigger.skip()The portal’s skip flow
flow.ship_nowgro.trigger.sendNow()The portal’s send now flow
flow.reschedulegro.trigger.reschedule()The portal’s reschedule flow
flow.pausegro.trigger.pause()The portal’s pause flow
flow.resumegro.trigger.resume()The portal’s resume flow
CodeWhen
SCOPE_DENIEDThe widget wasn’t granted the scope for this call
SETTING_DISABLEDThe store has turned this off in portal settings
VALIDATION_FAILEDBad input, e.g. a missing entity id or file
NOT_AVAILABLEThe trigger can’t run for this subscription right now
FLOW_BUSYAnother portal flow is already open
NEEDS_CLICKA trigger was called without a recent click or key press
RATE_LIMITEDToo many calls. Bursts of 5 are fine, then 1 per second
USER_CANCELLEDThe customer declined a confirm prompt, or you’re in preview
BACKEND_ERRORThe server rejected the change. Try again or show a friendly message
UNKNOWN_CAPABILITYThe 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.