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.
Lifecycle
Section titled “Lifecycle”gro.onReady(callback)
Section titled “gro.onReady(callback)”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});gro.onContextChange(callback)
Section titled “gro.onContextChange(callback)”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();gro.onBrandingChange(callback)
Section titled “gro.onBrandingChange(callback)”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);});gro.resize()
Section titled “gro.resize()”Re-measures your content and tells the portal how tall to make the frame. Call it after every render.
Properties
Section titled “Properties”gro.context
Section titled “gro.context”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 |
gro.branding
Section titled “gro.branding”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.
gro.scopes
Section titled “gro.scopes”The scopes your widget was granted, e.g. ["entity.update", "flow.skip"].
gro.isPreview
Section titled “gro.isPreview”true in portal preview. Nothing can change in preview: every action and
trigger rejects with USER_CANCELLED.
Triggers
Section titled “Triggers”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.
gro.trigger.can(name)
Section titled “gro.trigger.can(name)”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.
Entities
Section titled “Entities”gro.entity.get(id)
Section titled “gro.entity.get(id)”Returns the entity with this id from gro.context, or null. No network call.
gro.entity.update(id, changes)
Section titled “gro.entity.update(id, changes)”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 }),});Activity
Section titled “Activity”gro.activity.log(entry)
Section titled “gro.activity.log(entry)”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 |
gro.media.upload(file)
Section titled “gro.media.upload(file)”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.
Scopes
Section titled “Scopes”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 |
Error codes
Section titled “Error codes”| 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 |
Sandbox rules
Section titled “Sandbox rules”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.comandfonts.gstatic.com). The store’s theme font is loaded for you. - Images work from any
https:URL or adata:URL. - Render into
#gro-widget-root.