Skip to main content

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.
Built-in integrations stay the default. A custom provider only takes over a capability once you explicitly select it for your store; until then, everything runs exactly as it does today.

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 on order.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, safe ctx:
The 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 registered endpoint_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-Timestamp is in milliseconds, so it compares directly against Date.now().
  • An absent signature is not an exemption. Galactic Core sends no X-Signature at 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.
A verification pass ahead of any handling:
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:
Generate it in Integrations → Extensions on the extension itself, then give it to your provider as their webhook or callback URL. The token appears once when you generate it and is stored only as a hash, so a lost URL is replaced by generating a new one rather than looked up — generating again immediately invalidates the previous URL, which is also how you revoke one that has leaked. When a provider posts to it, Galactic Core authenticates the request, then invokes your extension with the provider’s payload as input.callback and operation set to callback. What happens next depends on your execution mode:
  • Remote — the payload is forwarded to your endpoint_url as 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 an input (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 / verify exactly as before. The provider field 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/calculate may return extra rates[] from your custom carrier and set rate_source to custom (see calculateShipping).
  • Email / SMS / Marketing / Sales channels — entirely off the storefront API (notification and sync internals); there’s nothing to call.
So the provider contracts above describe what your extension implements, not what the storefront API returns. The only storefront-API change in the whole feature is the shipping rate_source: custom value.

A social publisher

A SocialPublisher 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.
Three operations, all admin-plane — no shopper is ever waiting on them:
amount_cents is server-authoritative. It is resolved from the agency’s own price catalogue, not supplied by the browser. Charging a different amount does not change what the merchant is recorded as owing — it only creates a mismatch between your gateway and the platform’s ledger.

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.refunded

Payments

payment.succeeded · payment.failed · payment.refunded

Customers

customer.created · customer.updated

Products

product.created · product.updated · product.stock_low · product.out_of_stock

Gift cards

gift_card.issued · gift_card.redeemed · gift_card.expired

Carts & sync

cart.abandoned · feed.sync.completed · channel.sync.completed

Promotions

promotion.applied · promotion.created · promotion.activated · promotion.deactivated · pricing_rule.created · pricing_rule.activated · pricing_rule.updated · pricing_rule.deactivated

Content & collections

collection.created · collection.updated · post.published · lookbook.published · review.approved

Deletion & 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.connected

Feature availability

feature.status_changed (a capability crossed between available / awaiting_data / not_in_plan)
The store-lifecycle, content, and feature-availability triggers fire as the store grows rather than per transaction, so an automation can react to a promotion going live, a review being approved, or a licensed capability gaining its first data. Each event’s payload is documented in the Webhooks reference.

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.
Every extension is also protected by a per-extension circuit breaker — after too many consecutive failures it is automatically disabled and you’re notified — and hard timeouts on any hot-path hook. Outbound requests from a hosted function are filtered to public destinations to prevent them reaching internal infrastructure.

Budgets

Each budget below is a ceiling for its class. A timeout_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.
These ceilings apply to every individual call, so an endpoint’s slowest path is the one that determines whether it fits. A lookup for a reference the provider has never seen is often far slower than a normal call, and an endpoint sitting just under the ceiling on average fails intermittently, which surfaces as flakiness rather than as a misconfiguration.

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 unencrypted http:// 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.

Examples


Writing one without starting from a blank page

Two routes reach the same result, and both produce code you review before anything is installed. The AI Extension Builder takes a description of the integration and writes it, grounded in the deployed API surface and, by default, the current documentation of whatever provider you name — so it builds against the API as it is now rather than as the model last remembered it. The in-app assistant reaches the same builder conversationally. It suits a request you have not fully specified: it can ask which provider you use and what should happen when the integration fails, and refine after you have seen the result. It also proposes what is worth automating in the first place, reading what your store already has connected. It can additionally run what it wrote and read the outcome — the status, the error, and the log lines the code produced — and repair it from that rather than handing you something that has never executed. A test runs against your real runtime with your connected credentials, so an integration that calls an outside service genuinely calls it. Whatever it writes is saved switched off and unpublished, so nothing runs on a real event until it passes the publish checklist.

Choosing your approach