Custom Integrations — Extensions
Extensions let your own logic run inside Galactic Core’s own flows — reacting to store events, or standing in for a built-in integration at the seam where the platform calls one. Registration, signing, retries, revalidation and run logging are handled by the platform. (The other route to a Custom Integration runs your own service entirely outside the platform and drives it over the API: Bring Your Own Compute, for orchestration an Extension doesn’t cover.) If what you need is somewhere to record information the platform does not model, rather than logic to run, that is Custom Fields. With Extensions there are two things you can build:Automations
React to a store event —
order.paid, product.stock_low, cart.abandoned — with a no-code recipe or a small custom function. “When X happens, call my system / sync a field / send a message.”Custom Providers
Become the implementation Galactic Core calls for a capability — Email, SMS, Marketing, Tax, Shipping rates, Sales channels, Payments, or shopper sign-in — standing in for the built-in integration.
Where extensions live: you create and manage extensions in GC Admin under Integrations → Extensions — not through the storefront API. A recipe builder, a code editor, a remote-endpoint registration form, and a live run-log debugger are all there.
The two building blocks
Automations
An automation runs after the fact, off a real store event, and never blocks anything. You author it two ways:- A recipe — a small, validated JSON graph (
trigger → filter → transform → action) with no raw code. Ideal for the “notify my system / call an API onorder.paid” case. - A custom function — a small handler when you need real logic.
Custom providers
A custom provider — one you build and connect yourself — implements one of these nine named interfaces, and when selected is dispatched at that subsystem’s seam in place of the built-in path. (Advertising and Accounting sync are also provider-backed, but Galactic Core builds and runs those for you — see the note below the table — so they are not in this build-your-own set.)For single-active interfaces (Email, SMS, Tax, Marketing, Auth) selecting a custom provider replaces the built-in for that capability. Shipping adds a rate source next to the native ones; Sales channels are fully multi-active; Payments route per transaction, so your custom method runs alongside the native processors.
AuthProvider is remote-only and fail-closed. It verifies who a shopper is, so it must run as a remote endpoint you host, must be synchronous, and if it rejects a token or can’t be reached the request is treated as signed-out — never signed-in. It is the second of the two bring-your-own-auth modes: with the first, your backend verifies the shopper and signs an assertion Galactic Core checks (x-external-auth); with an AuthProvider, the shopper’s raw identity-provider token is sent to your verifier and Galactic Core trusts the identity it returns (x-idp-token). See Customers and Auth.Advertising and Accounting sync are platform-managed integrations — you connect them rather than build them. Advertising runs your Google/Meta ad campaigns (see Advertising); Accounting sync mirrors every posted ledger entry into QuickBooks so a store’s external books stay current automatically (see Accounting).
Execution modes: hosted vs remote
Every custom function and provider runs in one of two modes. The contract is identical — only where the code runs differs.Hosted
You write a handler; Galactic Core runs it in an isolated sandbox. Your provider secrets are held in our secure vault and injected at run time. Nothing to deploy or operate.
Remote
You run an HTTPS endpoint on your own infrastructure. Galactic Core calls it with a signed request and expects the same return shape. Your provider secrets never leave your side.
Hosted handler
A hosted function is a single default-export handler. Galactic Core passes it the event (or, for sync hooks, the hook input) and a small, safectx:
ctx surface is deliberately tiny — it’s the entire capability set:
There is no database client, no API key, and no access to any other store’s data in
ctx. If your function needs to change store data, it does so the same way any client does — ctx.fetch to the public API with your own key (supplied as a secret alias).
Remote endpoint
In remote mode, Galactic Core POSTs the same input a hosted handler would receive to your registeredendpoint_url, and your endpoint returns the same shape a hosted handler would return. The request is signed so you can verify it came from Galactic Core:
Three details govern whether verification succeeds:
- The signed input is the raw header string and the raw body bytes. Re-serialising the parsed JSON changes key order and spacing, and the signature will not match.
X-Timestampis in milliseconds, so it compares directly againstDate.now().- An absent signature is not an exemption. Galactic Core sends no
X-Signatureat all when an extension has no signing secret configured, so an endpoint that verifies only when the header is present accepts anything from a caller that omits it.
Timestamp / replay: requests carry a 5-minute freshness window — reject anything older. Retries: on a transient failure (a network error or a
5xx), Galactic Core retries the request once, re-signing with a fresh timestamp; a 4xx is treated as final and is not retried. Your endpoint_url must be HTTPS and publicly reachable — internal, link-local, and metadata addresses are rejected.Receiving callbacks from a provider
Some providers do not answer immediately. A mobile-money charge, a bank transfer, or an identity check finishes later and notifies you by posting to a URL. Every extension has one for this:input.callback and operation set to callback. What happens next depends on your
execution mode:
- Remote — the payload is forwarded to your
endpoint_urlas a signed Galactic Core dispatch, with the same headers and verification rules as any other remote call. Your endpoint verifies a request from Galactic Core rather than an unauthenticated one from the provider. - Hosted — your handler is invoked with the payload directly.
Choosing how the callback is authenticated
A public URL that runs your code needs to prove who is calling it, and providers differ in what they can offer. Three modes, configured per extension:
With
hmac, the signature is verified against the raw body before your code runs, and a request arriving
without the header is rejected rather than allowed through — the same rule that governs remote dispatch
in reverse. Bare digests, sha256=…, and t=…,v1=… header formats are all accepted.
Every rejected callback is recorded in the extension’s run log with the reason, so a provider
misconfiguration is visible rather than silent.
Acknowledgement is immediate. The endpoint answers as soon as the request is authenticated and
recorded, before your handler finishes. Providers re-deliver when a response is slow, so waiting on your
code would invite duplicate deliveries. Your handler’s return value is applied through the normal
provider contract, and money-bearing results are revalidated server-side — a callback asserting that a
payment succeeded does not by itself settle an order.Limits: 120 callbacks per minute per extension, and a request body up to 128 KB. A callback counts as
one invocation.
Provider contracts
Each interface operation receives aninput (with an operation discriminator where it has more than one) and must return the shape below. This is the same whether you run hosted or remote. Money-bearing returns (payment status, rate amounts) are revalidated server-side — see Safety.
Declining a payment provider name you don’t serve. Your payment extension is dispatched for
any provider name outside the built-in set, not only the ones you handle — a store can name a
gateway you have never heard of. When you are asked about a name you do not serve, return
{ unsupported_provider: true }: the shopper gets 400 Unsupported provider, which is accurate,
instead of an error that reads as your provider failing.Return it only when you genuinely do not serve that name. It also marks you as not owning the
payment, so returning it for a name you do serve makes the platform stop asking you to confirm
whether that payment succeeded.What a sales channel receives. Your channel extension does its own mapping, so
push hands you
the product as the platform holds it rather than a pre-shaped payload: identity (sku,
product_id, variant_id, name, description, brand), commerce (price, sale_price,
stock, link), media (image, media[]), taxonomy (category, subcategory), the attributes
a channel matches a listing against (gtin, condition), per-variant detail (variant_name,
variant_attributes), published specifications, and custom_fields — the merchant’s own defined
fields. Cost and margin are never included.custom_fields carries the fields a merchant shows on their storefront, plus any they have chosen
to send to your channel specifically. Treat it as optional: a store that defines none sends none,
and the names are the merchant’s own, so read the keys you recognise rather than assuming a fixed
set.How this relates to the storefront API. Connecting a custom provider does not change the
storefront API contract your app calls. A shopper’s storefront never sees “which provider handled
this” — the provider is invoked internally behind the endpoints you already use:
- Payments — you still call
POST /v1/payments/initialize/verifyexactly as before. Theproviderfield already accepts any name, and the response shape is unchanged, so a custom payment provider just means a provider name outside the built-in set is handled instead of rejected. (The platform still validates the amount and re-checks the paid status.) - Tax — resolved server-side; it only affects the order’s
tax_amount, which is already part of the order response. - Shipping — the one visible addition:
GET /v1/shipping/calculatemay return extrarates[]from your custom carrier and setrate_sourcetocustom(seecalculateShipping). - Email / SMS / Marketing / Sales channels — entirely off the storefront API (notification and sync internals); there’s nothing to call.
rate_source: custom
value.A social publisher
ASocialPublisher endpoint keeps the network’s credentials itself and answers four operations. This
one posts to a Discord channel webhook it holds; the caption, media, schedule and the publish-time
checks are handled before publish is ever called.
reconnect: true tells the merchant to reconnect the account; permanent: true fails the post
without retrying; any other failure is retried.
Billing your own merchants
SubscriptionBillingProvider is the one interface that is not about a shopper. It is how an agency
running its own commerce platform charges its merchants for their plan — the merchant pays the
agency, on the agency’s own payment account, and Galactic Core never touches that money.
Stripe, Lemon Squeezy and Paddle are supported directly, and Paystack in its markets, so an agency
using one of those connects it in their portal rather than building anything. A
SubscriptionBillingProvider covers the remaining cases — a regional gateway outside that set, or an
agency’s own billing system that already holds the customer relationship.
This is a different money flow from
PaymentProvider, which is a shopper paying a merchant at
checkout. Subscription billing is a merchant paying their agency for the platform. They use separate
interfaces so the two can never be resolved through each other’s seam: a shopper’s checkout is never
affected by an agency’s billing choice, and vice versa.Triggers
Automations subscribe to the same event vocabulary as webhooks. The events that fire today:Orders
order.created · order.paid · order.fulfilled · order.shipped · order.cancelled · order.updated · order.refundedPayments
payment.succeeded · payment.failed · payment.refundedCustomers
customer.created · customer.updatedProducts
product.created · product.updated · product.stock_low · product.out_of_stockGift cards
gift_card.issued · gift_card.redeemed · gift_card.expiredCarts & sync
cart.abandoned · feed.sync.completed · channel.sync.completedPromotions
promotion.applied · promotion.created · promotion.activated · promotion.deactivated ·
pricing_rule.created · pricing_rule.activated · pricing_rule.updated · pricing_rule.deactivatedContent & collections
collection.created · collection.updated · post.published · lookbook.published · review.approvedDeletion & restore
product.deleted · promotion.deleted · collection.deleted · post.deleted ·
lookbook.deleted · pricing_rule.deleted · subcategory.deleted · customer.deleted —
the record has left the API. Each has a matching .restored, since deleting is reversible for
90 days; the payload carries restorable_until.Visibility
category.activated · category.deactivated · subcategory.activated ·
subcategory.deactivated — a category switched off is filtered out of every catalog read, so it
leaves a storefront just as a deleted one would.Store lifecycle
store.updated · store.profile_updated · store.policy_published · store.policy_unpublished · store.design_system_updated · payment_provider.connected · shipping_provider.connected · channel.connectedFeature availability
feature.status_changed (a capability crossed between available / awaiting_data / not_in_plan)Safety
Custom code sits on paths a sale runs through, so the platform bounds what a failure in it can reach:Automations never block
Automations run after the event that triggered them. A slow or failing automation can never delay or block checkout, an order, or any request.
Sync providers fail safely
A
ShippingProvider or TaxProvider that errors or times out falls back to the native path — the sale continues. A PaymentProvider failure returns provider unavailable with no order created or changed — never a false “paid”.Opt-in gates
order.validate is fail-open by default (a buggy rule never blocks sales). You can opt a specific gate into fail-closed when you need a hard compliance stop.Galactic Core stays the authority
A custom provider’s claimed payment status and returned rate amounts are revalidated server-side and never trusted as final. The platform remains the price, payment, and ledger authority.
Budgets
Each budget below is a ceiling for its class. Atimeout_ms on your extension can only make its
budget smaller, never larger, so an endpoint written against a longer budget is cut off at the
limit for the class it runs in.
A shopper-facing hook runs while a page render waits on it, which is what sets those budgets in
hundreds of milliseconds. The admin-plane budget is larger because a merchant has clicked a button
and the work happens in front of them: a billing provider makes two round trips, authenticating then
creating, which does not fit inside a checkout budget.
Code review
Stored handler source is scanned on a schedule, and on demand from the Extensions page. The scan reads the code as text — it does not run it — and separates what it finds into two kinds. Corrected automatically. A fault with a single correct answer is repaired in place, because the exposure grows for as long as it stands. A provider key written directly into source is replaced with a placeholder, and a request addressed to an unencryptedhttp:// endpoint is moved to https://. Each
change is recorded with a before-and-after, and the stored before-image has the credential stripped out
of it, so the record of the fix does not preserve the thing it removed. A correction is made to the
saved version; on a live extension it reaches production when that version is published. A redacted key
means the handler will now fail to authenticate rather than continue running on an exposed
credential — connect it under Integrations and read it by name.
Reported for you to decide. Anything where a reasonable author could disagree is left alone and
raised as a suggestion, since a rewrite changes what the extension does. The most common one is reading
a connected credential as ctx.secrets['name'] rather than await ctx.secrets.get('name'): the first
form returns nothing, so the request goes out unauthenticated and the provider’s refusal looks like an
account problem rather than a bug.
Scanning and automatic correction are separate settings. Turning correction off keeps the scan and
reports everything instead, including the faults that would otherwise be repaired. Where a store is
managed by an agency, the agency decides whether its merchants are scanned.
The scan reasons about text, so it recognises the patterns above rather than understanding your
logic. It is a floor under the obvious mistakes, not a substitute for reviewing what an extension
does with the data it touches.
Publishing
A production extension has two versions: the one you saved and the one that is live. Saving changes only the saved version. The live version keeps serving checkouts, events, and callbacks until the saved version passes the publish checklist, at which point it becomes the live version in one step. A sandbox extension has no separate live version and runs as saved. The checklist is computed by the platform against the exact saved version, identified by a hash of everything that executes: the code or recipe, the endpoint, the connected credentials, the time limit, and the failure behaviour. Any later change produces a different version, which needs its own test before it can be published.
The last check is made live. The platform sends your endpoint a request in the normal dispatch format
whose
X-Signature is computed with a key that is not yours, and it passes only when the endpoint
answers with a 4xx other than 429. An endpoint that accepts it would act on a request anyone could
send.
A critical or high finding always blocks publishing; medium findings are listed as suggestions and do
not. A hosted version’s code is deployed only after every check has passed, under a name of its own,
so a failed attempt never replaces the code that is live. A production extension cannot be switched on
until a version has been published, and while it is live its type, trigger, provider role, and
environment cannot change without turning it off first. These rules are enforced by the database for
every writer, not only by the Extensions page.
The same checklist applies to extensions written by the AI Extension Builder and the
in-app assistant. Text the builder reads while writing, such as a provider’s documentation page, is
treated as data rather than as instructions; the checklist and the outbound request filter are what
stand between generated code and production either way.

