OctoWiki
Vendor AdaptersTop-up Vendors

DT One (Top-up)

Direct-topup vendor adapter — mobile recharge over DT One DVS, Basic auth, async transactions, poll-driven (no webhook by default), reference min/max/discount mapping.

Quick facts

Code DTONE (service_id=1, Mobile) · Line direct-topup (mobile recharge) · Auth HTTP Basic · Model async (/v1/async/transactions, auto-confirm) · Webhook supported but not seeded (poll-driven by default) · Lookup yes · Adapter services/external_vendors/direct_topup/dtone/

DT One DVS mobile recharge. The eSIM sibling is a separate adapter — see DT One eSIM. DT One is the reference implementation for min/max/discount mapping. Registered direct_topup/factory.go:178. See Order Lifecycle.

Auth

HTTP Basic (username/password). Credentials: host, username, password, optional service_id (defaults "1"). Prod host https://dvs-api.dtone.com. Default timeout 30s.

Catalog

  • GetCategoriesGET /v1/operators?service_id=1&per_page=100 → one Category per operator; each carries a required mobile_number phone input (regex ^[0-9]{7,15}$, "without country code"). Image from operator-logo.dtone.com/logo-%d-2.png.
  • GetProducts(operatorID)GET /v1/products?operator_id=....
  • mapProduct (the reference): IsFixedAmount = HasPrefix(Type, "FIXED_VALUE_"); when fixed, MinAmount = MaxAmount = Destination.Amount; VendorDiscount = ((retail − wholesale)/retail)×100; Price = Wholesale.Amount. Rich buildVariantFields metadata (benefits, validity, pricing, fx_rate, DT-One ids). ShouldSyncVariants()=false (lazy); SupportsExternalOrderRef()=true.
  • Lookup: SupportsLookup()=truePOST /v1/lookup/mobile-number returns operators (first flagged Identified).

Order create

POST /v1/async/transactions {ExternalID: merchantOrderID, ProductID, AutoConfirm:true, CreditPartyIdentifier: InputData} (+ CallbackURL only if configured). Error decision tree: 1007001 duplicate → ErrDuplicateMerchantRef; transient 408/429/5xx → leave status empty (stay PENDING, don't burn the order/refund on a blip); parseable error → FAILED with failure fields; network → PENDING.

Order status / poll

GET /v1/transactions/?external_id={merchantOrderID}. 1008004 not-found → ErrOrderNotFound; empty array → ErrOrderNotFound; CompletedAt from ConfirmationDate. Poll driver: jobs/topup_order_retry_job.go.

Webhook

No default webhook. The topup seeder omits callback_url, so recharges are poll-driven out of the box. VerifyWebhookSignature always returns true — DT One documents no signature; defence is a per-merchant secret URL + IP allowlist. Route POST /webhooks/direct-topups/:vendor_code.

Two webhook persistence paths. The active path writes to vendor_webhook_logs (idempotency key vendorOrderID:merchantOrderID:status). The direct_topup_webhooks table + repo appears to be legacy/dead code — no non-repo callers found. Confirm with the team before relying on it.

Delivery / balance

Recharge lands directly on the subscriber's mobile_number — no PIN/QR; delivery is expressed purely via status + ConfirmationDate. GetBalance = GET /v1/balances ([0].Available); Ping = GET /v1/services/1.

Failure taxonomy & INSUFFICIENT-BALANCE

mapStatus treats REJECTED/DECLINED-INSUFFICIENT-BALANCE, -OPERATOR-CURRENTLY-UNAVAILABLE, and -DUPLICATED-TRANSACTION as transient → PENDING (matched before the generic REJECTED/DECLINED fallback), so the retry loop keeps polling and no failure_code is written. HTTP code 1006001 (float low) → FailureUnknown (pages ops rather than hard-failing the customer). Sub-status table maps INVALID/BARRED/INELIGIBLE-CREDIT-PARTY and limit codes to canonical failures. Sentinels 1007001/1008004 excluded from the HTTP map and handled specially; 5xx/429/408 entries are effectively dead code (short-circuited by isTransientHTTPStatus).

Tests, mock, docs

Contract (dtone_contract_test.go, build tag contract) + error-mapping unit test, against mocky (frontend/mocky-balboa/src/routes/dtone.ts, serves both services). Docs under docs/dtone/. Live docs: dvs-api-doc.dtone.com.

On this page