OctoWiki

Vendors & Integrations

The vendor adapter architecture, the full adapter inventory, order orchestration, inbound sales channels (Shopify, G2A), and OCTO federation.

Octopus talks to the outside world through vendor adapters under services/external_vendors/. There are three outbound product lines — voucher, direct-topup, eSIM — each with its own interface, factory, and adapters. Plus two inbound sales channels (Shopify, G2A) where Octopus is the server, and a payouts family.

This page covers vendor adapters and orchestration. For how vendor products are modelled, priced, made visible (blacklists), and kept in sync (catalog sync, wallet balance), see Products, Vendors & Catalog.

Never leak vendor topology to customers

Vendor brand names and the generic supplier vocabulary ("vendor", "upstream", "supplier", "submit to vendor", "vendor outage") must never appear in any client-facing surface — API fields, docs, emails, the client panel, the claim page, webhooks. Admin/ops surfaces only. Grep before shipping. See the Memory Appendix.

The two vendor concepts

"Grasshopper" is not separate from topup vendors — it's simply one voucher vendor (code='GH'). The real axis is product line + vendor.code.

The vendors table (database/models/vendor.go) carries:

  • code *string — the routing key every factory switches on.
  • vendor_type: VOUCHER | DIRECT_TOPUP | BOTH | ESIM.
  • Orchestration flags: is_async, has_webhook, has_reference_id_support, pre_fetch_enabled, has_api, is_bulk, is_active, max_quantity, p_limit, currency_code, balance.

Credentials live in the DB, not env

All vendor API credentials/config are rows in vendor_attributes (vendor_id, key, value). Factories map well-known keys (host, username, password, api_key, callback_url, timeout) onto a VendorConfig, and pass everything else through Extra/Credentials maps (e.g. NeoCurrency client_id/client_secret, Wupex merchant_code, OCTO self_base_url/webhook_secret). Exceptions: Shopify creds sit on the clients table (encrypted); G2A creds live in g2a_credentials (bcrypt).

Adapter inventory

Each factory switches on vendor.code. Codes are types.VendorCode constants in services/external_vendors/types/common.go.

CodeNameLineAsync / styleAdapter file
TRSThe Reward Storevouchersync code deliveryvouchers/trs_vendor.go
RUNARunavoucherasync, link-based; Svix-signed webhook or pollvouchers/runa_vendor.go
WPXWupexvoucher (+topup)pollvouchers/wupex_vendor.go, direct_topup/wupex/
IRWiRewardifyvoucherwebhook (custom payload)vouchers/irewardify_vendor.go
EPINEpinForcevoucherasync, NO webhook — stateful shared cart, poll-only, XLSX/ZIP deliveryvouchers/epinforce_vendor.go
GHGrasshoppervoucherCF-Worker redemption; Octopus mints code+PIN, sends SHA-256 hashes to GHvouchers/grasshopper_vendor.go
NEONeoCurrencyvoucherpoll (lazy OAuth2 bearer)vouchers/neo_vendor.go
OCTOOctopus Network (federated)voucherasync + HMAC webhook + poll, login-tokenvouchers/octopus_vendor.go
SEAGMSEAGMtopuppoll (HMAC-SHA256 signed)direct_topup/seagm/seagm_vendor.go
DTONEDT One DVStopupasync (webhook + poll)direct_topup/dtone/dtone_vendor.go
OCTO_TOPUPOctopus Network (federated)topuplogin-token, self-loop guardeddirect_topup/octopus/octopus_vendor.go
DTONE_ESIMDT One DVS eSIMesimasync (poll + install poll)esim/dtone/dtone_esim_vendor.go
OCTO_ESIMOctopus Network eSIMesimlogin-token; webhook = poll-nudge onlyesim/octopus/octopus_esim_vendor.go

types/common.go declares many more voucher codes (LC, BNC, RZP, WOGI, NCTV, DIGC, VCHG, AQP, GLT, FLZ, WC, TRF) that are not implemented — the factory only handles the ones above. Treat them as reserved/planned. Don't add speculative branches for product types you don't actually receive (house rule).

Shared infra: vendorhttp/ (common HTTP client + config), octopuscommon/ (self-loop guard.go + login-token token_manager.go for the OCTO family), webhooks/ (Svix + HMAC verifiers), types/failure_codes.go (normalized failure taxonomy). payouts/ (Ledig, Merit) is a separate outbound-money family, not product vendors.

Order orchestration

Three paths, one shape: create → (poll / webhook) → complete, with state on the order/recharge/esim_order row's sub_status.

Vouchers — two sub-flows

Prefetch (async vendors, pre_fetch_enabled) — the platform pre-orders voucher stock ahead of demand and caches it. Driven by a formal state machine in jobs/prefetch_state_machine.go:

validTransitions is the authoritative graph. Retries use exponential backoff (2^n min, capped at 16); isRetryableError matches VENDOR_TIMEOUT/RATE_LIMIT strings. Terminal stage PUMPING pushes inventory to Valkey.

Live orders (not pre-stocked)jobs/pending_order_retry_job.gohandler.ProcessOrderRetry(...) creates order items, assigns inventory, calls the vendor, updates delivery. Vendor calls go through services/external_vendors/manager.go (VendorManager), which resolves and caches the adapter.

State lives on order_items.status + order_items.sub_status. There's a recreate protocol: types.ErrShouldReCreate / FormattedOrderResponse.ShouldReCreate — when a vendor loses record of an order (terminal/404), the orchestrator clears vendor_order_id and re-CREATEs on the next retry while the Octopus order stays PENDING.

Topups

services/topup_order.go CreateTopupOrder: pick best vendor variant → debit client wallet (or burn a redeemed voucher via redeem_resource_id) → create recharge atomically → call the vendor. Completion polled by jobs/topup_order_retry_job.go (batch 50, max 10 retries).

eSIM

services/esim_order.go (~1220 lines). Completion driven by jobs/esim_order_retry_job.go (poll), jobs/esim_installation_poll_job.go (activation), jobs/esim_expiry_job.go.

Inbound webhooks

services/vendor_webhook_service.go ProcessWebhook(vendorID, req) routes by resource_type (order/recharge/payment). Each adapter normalises its native payload to types.NormalizedWebhookPayload. Signatures verified via webhooks/ (Svix or HMAC). Events persisted (idempotency via IdempotencyKey).

Notification discipline

Webhooks/emails fire only at terminal (status, sub_status) pairs — never on in-flight sub-statuses like VENDOR_ORDER_PENDING, even when the top-level status is FAILED/CANCELLED. See the Memory Appendix.

Inbound sales channels

These run the opposite direction — external systems buy from Octopus. Not in external_vendors/.

Shopify — services/shopify/

Octopus is a client polling Shopify. ClientFactory builds one Shopify GraphQL client per tenant using per-client creds on the clients table (ShopifyShopName, encrypted ShopifyAccessToken, ShopifyAPIVersion, ShopifyEnabled/SyncEnabled). Cron jobs pull and fulfil orders (fetch-shopify-orders, process-pending-shopify-orders, detect-shopify-cancellations), fulfilling through the internal voucher pipeline. There's a locked upgrade plan to Shopify API 2026-04 — see the Memory Appendix.

G2A — http/handler/g2a*.go

Octopus is the HTTP server and OAuth2 token issuer; the G2A marketplace calls in. GET /g2a/oauth/token issues bearer tokens (client_credentials; creds in g2a_credentials). Flow: reserve → order → pull codes. Reservations hold no codes — Octopus Cards (GH) are minted on confirm, not drawn from a finite pool; the g2a-reservation-expiry cron expires stale holds (30m TTL). Product mapping via g2a_product_mapping. See the Memory Appendix.

Shopify = Octopus is an outbound client (pull). G2A = Octopus is an inbound server (push). Both are multi-tenant / client-scoped.

OCTO federation (self-vendor)

Octopus can consume another Octopus instance's public /api/v1 as if it were a third-party supplier, across all three lines: OCTO (vouchers → /api/v1/orders), OCTO_TOPUP (/api/v1/topups), OCTO_ESIM (/api/v1/esim). Auth is a login flow (not a static key): POST /auth/login → short-lived tokens, managed by octopuscommon/token_manager.go. octopuscommon.SelfLoopGuard errors if a federated vendor's host resolves to this instance's own base URL, preventing an order recursing into itself. Full contract gotchas (bare-array lists, write-only client_reference, composite productID:variantID VendorVariantID, eSIM webhook omitting activation_code) are in the Memory Appendix.

Catalog sync & balances

  • Catalog syncservices/vendor_catalog_service.go; vendors stream products via GetCatalogBatched. Crons: vendor-catalog-sync (daily 2 AM), vendor-catalog-cleanup (Sun 3 AM), vendor-catalog-retry (every min).
  • Balance syncvendor-balance-sync (every 15 min) pulls GetBalance into vendors.balance / vendor_wallet.
  • Inventory pumpinventory-pump (every 6h) or go run main.go pump-inventory pushes stored voucher inventory into Valkey.

Vendor adapter shipping rule

Every new services/external_vendors/ adapter ships with a mocky-balboa route (incl. chaos hooks for 4xx/5xx/timeout/malformed/drop) plus unit + contract + service-level tests in the same PR. Every direct_topup mapper must populate MinAmount/MaxAmount/VendorDiscount on types.Product (DT One is the reference). See the Memory Appendix.

On this page