OctoWiki
Vendor AdaptersTop-up Vendors

Octopus (OCTO_TOPUP) — federated top-up

Direct-topup vendor adapter consuming another Octopus's /api/v1/topups — login-token auth, composite variant IDs, mobile lookup.

Quick facts

Code OCTO_TOPUP · Line direct-topup · Auth login-token (octopuscommon.TokenManager) · Model async (create → poll/webhook) · Webhook HMAC (in-adapter) · Lookup yes · Adapter services/external_vendors/direct_topup/octopus/

The top-up member of the OCTO federation family — consumes another Octopus's /api/v1/topups. Registered direct_topup/factory.go:182. Uses the shared octopuscommon TokenManager + SelfLoopGuard (see the voucher OCTO page and self-vendor memory).

Auth

Shared octopuscommon.NewTokenManager (login/refresh single-flight) bridged into vendorhttp via a race-safe bearerAuth (atomic.Pointer[string]); doAuthed re-auths once on 401. Credentials: host (→ APIBaseURL), username, password, webhook_secret, self_base_url.

vendorhttp gotcha: it returns a non-nil resp (with StatusCode) alongside an error for non-2xx, so the 401 check inspects the status even when err != nil — before returning on the error.

Catalog

Generic bare-array pager (X-Total-Pages/X-Has-More). GetCategories = GET /api/v1/topups/products; GetProducts = GET /api/v1/topups/products/{id}/variants. LookupMobileNumber = POST /api/v1/topups/lookup {mobile_number}bare array of operators. SupportsLookup()=true, SupportsExternalOrderRef()=true, ShouldSyncVariants()=false (catalog seeded, not pulled).

Composite VendorVariantID = "productID:variantID". Upstream create takes the parent product_id + amount and auto-selects the variant, so upstreamProductID() parses the prefix. The parent id is also stashed as a variant field octo_product_id.

Order create

POST /api/v1/topups/orders {product_id, amount, input_data, client_reference} (MerchantOrderIDclient_reference). 2xx → parse; 400 + duplicate → ErrDuplicateMerchantRef; transient (0/5xx/429) → leave status empty (PENDING/retry); other 4xx → terminal FAILED.

Order status / poll

By id GET /api/v1/topups/orders/{id} (404 → ErrOrderNotFound); recovery by GET /api/v1/topups/orders?client_reference={merchantOrderID} → bare array. Status map: DELIVERED/RECHARGED/COMPLETE(D)→complete, FAILED/CANCELLED/PROCESSING, else PENDING; surfaces transaction_id, failure_code/reason, is_user_fixable, completed_at.

Webhook

ParseWebhook handles topup.delivered/failed/cancelled ({id, type, data{id, client_reference, status, failure_*}}); MerchantOrderID = data.client_reference. VerifyWebhookSignature = hex HMAC-SHA256; blank secret ⇒ returns true (unverified).

Delivery

No code deliverable — completion status + transaction_id flow back via poll/webhook.

Quirks

Composite VendorVariantID; bare-array lists; write-only client_reference; ShouldSyncVariants=false; self-loop guard.

Tests

direct_topup/octopus/octopus_vendor_test.go, octopus_orders_test.go, octopus_fuzz_test.go, octopus_soak_test.go (goleak + -race, 32 workers), octopus_sandbox_test.go. E2E: test/clientapi/octopus_topup_orchestration_test.go (build tag orchestration). Uses test/octopusfake/ (topup order engine). Webhook-body gotcha: use testhelpers.WithRawBody, not WithBody (which base64-marshals and breaks the signature).

On this page