Phase 0 Research: UnivaPay Card Payment Integration (MVP)

All items below were unknowns or open design questions in the spec. Each is resolved by grounding
in the official UnivaPay documentation (https://docs.univapay.com/, fetched 2026-08-03), the
existing Yoyacoo Stripe implementation (referenced via docs/956-Customer-Subscription/ and
docs/1023-grace-period-7-days/), and UnivaPay’s answers to the Yoyacoo team (recorded in the
feature request).

1. What is UnivaPay’s object hierarchy?

  • Decision: Model the integration around UnivaPay’s hierarchy: Merchant (加盟店) → Store (店舗)
    → App Token → Webhook
    . In current UnivaPay operations one merchant registers one store, so in
    practice a supplier = one store.
  • Evidence (认证 / docs/api/general/authentication/): “現在は1つの「加盟店」に対し複数の「店舗」を登録しない運用をしていますので、現実的には「加盟店」権限のアプリトークンは発行・活用の価値がありません。” — merchant-type app tokens are useless in practice; always use store tokens.
  • Implication: The app token is the store’s key — “アプリトークンとは、どの店舗で決済を行うかの判別のためのキー”. The Store ID may be auto-retrieved from the token via the Store API (GET/LIST), which is a candidate simplification for the settings form (see spec Assumptions).

2. How is authentication done?

  • Decision: Backend API calls use HTTP Authorization: Bearer {secret}.{jwt} (secret + token
    concatenated with a dot). Browser/inline-form requests use the JWT only, and are additionally
    authenticated by registered domain.
  • Evidence: docs/api/general/authentication/ — “Bearer {secret}.{jwt}”; “ウィジェットやインラインフォーム出力リクエストは、登録したドメインからであることを認証しています。管理画面で「店舗」のアプリトークンを作成する際には、ドメインを登録してください。”
  • Key consequences:
    • The Secret is shown only once at token creation (「シークレットは、アプリトークンを作成した時だけ表示されます」). Yoyacoo’s settings screen must tell the supplier to copy it immediately; if lost, the token must be rotated.
    • The Secret is NOT needed for the inline form itself, but IS required for backend API calls in scope (refunds, cancels, charge GET) — collecting it is justified.
    • Domain registration is mandatory for the inline form to load. The supplier must register the Yoyacoo customer-site domain (staging and production separately) on the store app token in the UnivaPay console. This is FR-005 in the spec.
  • Verification check (FR-003): feasibility confirmed — an authenticated Store GET request with the submitted credentials either succeeds (Connected) or fails with an auth error (Unavailable).

3. What is the webhook contract?

  • Decision: Webhooks are created by the supplier in the UnivaPay console (管理画面>ウェブフック>ウェブフック追加). Yoyacoo displays the receiver URL and an Authorization value; the supplier pastes them in, selects triggers, and saves. Webhooks can also be managed via API (GET/UPDATE) — not required for MVP.

  • Evidence: docs/guide/detail/webhook/ — webhooks POST to a merchant-specified URL; local URLs are not allowed; the merchant defines an arbitrary auth_token (6+ chars) that UnivaPay sends in the Authorization header on every delivery.

  • Events relevant to this feature (from the event list):

    Event Trigger Status values used
    charge_finished One-time charge processing completes successful, failed, error, canceled (canceled = refunded)
    refund_finished Refund completes successful, failed
    cancel_finished Cancel completes successful, failed, error
    charge_updated Charge reaches authorized (not used in MVP — immediate capture)
  • Reliability rules (must be honored):

    • Best-effort delivery — UnivaPay officially recommends not relying solely on webhooks for payment determination; the spec keeps webhook-centric operation per UnivaPay’s answer, with a manual “check status” enhancement candidate (spec Edge Cases).
    • Retry policy: 2xx = no retry; 3xx = stop; 4xx/500-502 = up to 10 retries then stop; other 5xx/timeouts = up to 10 retries, never stops. Duplicates are possible → receiver MUST be idempotent (record request IDs, return 200 for replays).
    • 3-second rule: receiver must return 200 within 3 seconds or UnivaPay treats it as a failure and retries → acknowledge fast, process asynchronously afterwards (queue, per constitution).
  • Per-store verification implication: unlike Stripe (one env-configured webhook secret), each supplier’s webhook has its own auth_token. Yoyacoo must store it per connection and verify incoming webhooks against the store_id + stored auth_token. This is FR-004 and must not be overlooked in planning.

  • Identifying the application: the payload contains metadata (passthrough of the metadata set at transaction-token creation) — used to match the Yoyacoo application ID (FR-009/FR-010).

4. What does the inline form require?

  • Decision: Use the UnivaPay inline form (インラインフォーム), which is credit-card only
    (「インラインフォームはクレジットカード決済のみを行うことができ、他の決済は利用できません」).
  • Evidence: docs/guide/implement/iframe/ — inline form displays the card input UI in an iframe; card data never touches Yoyacoo’s servers (PCI handled by UnivaPay); card tokenization + processing happen on UnivaPay’s side; completion flows to form submit, webhook, and API retrieval.
  • Key consequences:
    • Scoping the MVP to card-only is not just a product decision — it is what the inline form supports.
    • The inline form must be served from a registered domain (see #2).
    • Installments: count selection happens inside the form (per UnivaPay’s answer) — Yoyacoo only needs the ON/OFF flag (FR-014).
    • 3-D Secure follows UnivaPay’s default rules (no custom control in MVP).

5. How are refunds and cancellations executed?

  • Decision: Use the UnivaPay Refund API for refunds and the Cancel API for cancels
    (one-time charge). Results arrive via refund_finished / cancel_finished webhooks and are
    reflected in application status + emails, mirroring the existing Stripe flow (FR-012/FR-013).
  • Evidence: API reference has docs/api/refunds/ and docs/api/cancels/ with CREATE endpoints and successful/failed statuses; webhook samples for both events exist in docs/guide/detail/webhook/.
  • Note: A charge whose status becomes canceled means the charge was canceled/refunded — status mapping during webhook handling must treat canceled as refunded.

6. How do test and production environments work?

  • Decision: Separate app tokens per environment — test-mode tokens for staging (FR-015),
    production tokens for production. Webhook payloads carry a mode field (test / live /
    live_test) which should be validated to prevent cross-environment contamination.
  • Evidence: docs/guide/first/setup/ — 「テストモードのアプリトークンを作成することでテスト決済が可能です」; test card numbers are available in the console (テスト課金); payload samples show "mode": "test".
  • Consequence: The supplier must create at least two store app tokens (test + live), each with
    its own domain registration and webhook configuration.

7. What is the existing Stripe pattern this feature mirrors?

  • Evidence (Yoyacoo codebase docs):
    • Per-supplier credentials live in user_payment_settings.stripe_connect_id (docs/956-Customer-Subscription/Summary.md).
    • Stripe webhooks land on the admin host only (/stripe/webhook, /stripe/connect, /stripe/account); customer/user apps 404 the default webhook routes (docs/956-Customer-Subscription/deployment/subscription-staging-deploy-checklist.md).
    • Webhook signatures verified via env-configured secrets (Cashier / custom middleware).
    • API wrapper = traits in common/src/Base/Traits/ (StripeConnectPayment, StripeConnectSubscription, StripeInvoiceHelper).
    • Orders carry Stripe refs (stripe_subscription_id, stripe_invoice_id); ServiceMenu carries stripe_price_id.
  • Implication for UnivaPay: introduce a UnivapayPlatform trait (createCharge/refund/cancel/verifyCredentials) in the same location, a per-supplier user_payment_univapays table, orders.univapay_charge_id, and a shared admin-host webhook endpoint. The one structural difference: webhook verification is per-store (auth_token stored per supplier), not a global env secret.

8. How does a supplier sign up for UnivaPay?

  • Decision: There is no self-service signup. The production merchant account is issued by
    UnivaPay through an application + human screening (審査) process. Yoyacoo cannot create it on
    the supplier’s behalf in the MVP (no OAuth/onboarding API). The Yoyacoo payment settings screen
    must therefore provide an application entry point (FR-001) linking the supplier to UnivaPay’s
    signup channels, then have them return with the issued credentials.
  • Entry channels (UnivaPay official site, univapay.com):
    • 導入のご相談 / お問い合わせ: https://univapay.com/contact/
    • デモアカウント申請 (instant demo/test account): https://univapay.com/form-demo/
    • After contact, UnivaPay sends the formal application form (申込フォーム) with manuals; in-house
      staff perform the screening (“有人での審査サポート”).
  • Instant test account: UnivaPay issues an account for operational testing immediately
    (審査前・審査中に運用テストができるアカウントを即時発行) — a supplier can obtain test
    credentials before production screening completes. Directly supports FR-016 (staging test
    tokens) without blocking on screening.
  • Do NOT point suppliers at ips-support@univapay.com for signup: per the docs’ お問い合わせ
    page that mailbox is for existing merchants only (login ID + merchant ID required for
    identity verification).
  • Notes:
    • The FAQ states each separate service requires its own screening/account — a supplier running
      multiple UnivaPay stores registers each connection separately.
    • UnivaPay’s usage guide mentions that “depending on the system integration partner, simple
      settings only may suffice” — Yoyacoo establishing a 連携先 (integration partner) arrangement
      with UnivaPay may simplify the supplier’s setup; worth confirming with UnivaPay during planning.

Open questions to resolve in planning

  • Whether Store ID remains a manual input or is auto-retrieved from the app token (Store API).
  • Webhook receiver endpoint placement (admin host, matching Stripe) vs. a dedicated route on the customer host.
  • Whether a reconciliation job (charge GET for applications stuck in “payment processing”) ships in MVP or is deferred.