Octopus (OCTO) — federated voucher
Voucher vendor adapter that consumes another Octopus instance's /api/v1 as a supplier — login-token auth, self-loop guard, free HMAC webhook.
Quick facts
Code OCTO · Line voucher · Auth login-token (POST /auth/login → bearer) · Model async + HMAC webhook + polling · Delivery code/pin/claim_url · Adapter services/external_vendors/vouchers/octopus_vendor.go, octopus_types.go
"Federation" = one Octopus instance consuming another Octopus's public /api/v1 as if it were any third-party supplier. Three sibling rows share the pattern: OCTO (voucher, here), OCTO_TOPUP, OCTO_ESIM. See the self-vendor memory and Order Lifecycle.
Federation internals (shared)
services/external_vendors/octopuscommon/ holds the shared SelfLoopGuard (errors if the federated host resolves to our own base URL — self_base_url attribute → APP_BASE_URL fallback) and TokenManager (mutex-guarded login/refresh single-flight; /auth/login + /auth/refresh; 60s skew; access/refresh expiry fallbacks now+1h/now+7d). The topup/esim adapters use TokenManager; the voucher adapter predates it and carries its own equivalent inline token lifecycle — it only imports octopuscommon for the guard.
Auth
Login-token: POST {host}/auth/login {username, password} → bearer, cached and refreshed; doAuthed re-auths once on 401. vendor_attributes: host, username, password, self_base_url (self-loop guard), webhook_secret (read by the webhook verifier factory, not the adapter).
Catalog
GetCatalogBatched — GET /api/v1/products?page=N, decoded as a bare array ([]octopusProduct); pagination via X-Total-Pages/X-Has-More headers. Each product flattens to one Product per available_denominations[] (fixed → max=min).
Order create
POST /api/v1/orders {product_id, denomination, quantity, client_reference}. 2xx → parse + return OrderID + any sync vouchers. 400 + duplicate-reference → stay PENDING/retry (order already exists upstream). 5xx/429 → PENDING; other 4xx → terminal FAILED.
client_reference is write-only (never echoed). GenerateReferenceCode returns a stable ref (ignores retry count) so a re-POST after a flaky network dedups upstream instead of minting a second order. Consequently, unlike most adapters, this one never sets ShouldReCreate — FAILED/CANCELLED is terminal (recreating would hit the stable reference as a duplicate).
Order status / recovery
GET /api/v1/orders/{id} (404 → ErrOrderNotFound). Reference recovery: with a nil vendor order id, GET /api/v1/orders?client_reference=REF (client-scoped, existing list endpoint — no new API); hit → resume, empty → create fresh with the same ref.
Webhook — free HMAC
NormalizeWebhookPayload parses {id, type, data{id, client_reference, ref, status, sub_status}} for order.delivered/partially_delivered/failed/cancelled, surfacing client_reference as the resource reference for orphan recovery. Signature verification is free — done upstream by the generic HMAC verifier (X-Signature = hex HMAC-SHA256(body, webhook_secret)); set webhook_secret to the same value as the upstream client's signing token to activate (blank → unverified).
Delivery
Voucher{Code: card_number, Pin: pin_code, ClaimURL: claim_url} from the order items; default expiry now+1yr. CancelVoucher unsupported. GetBalance returns a bare wallet array but only numeric currency_id (no ISO code) so Currency is left empty.
Quirks
Bare-array lists; write-only client_reference; stable ref (no retry suffix); self-loop guard; FAILED/CANCELLED terminal (never re-create).
Tests
vouchers/octopus_vendor_test.go + octopus_sandbox_test.go (build tag sandbox, read-only unless OCTOPUS_SANDBOX_ALLOW_ORDER=true). The federation family shares the test/octopusfake/ in-process programmable upstream (chaos engine, order engines, SignWebhook) — bound via testhelpers.BindVendorToOctopusFake.
Grasshopper — GH
Voucher vendor adapter — Octopus's own inverted-flow claim system. Octopus mints code+PIN and ships only SHA-256 hashes to a Cloudflare Worker.
SEAGM
Direct-topup vendor adapter — gaming recharge, HMAC-SHA256 query-string signing, async with signed inbound webhook and a rich failure taxonomy.