AnnouncementsIntroducing Arkoren

    Menu

    Payment Drivers

    Choose and configure a payment backend, with optional provider notifications and local call monitoring.

    What's on this page
    1. Compare Memory, Stripe, PayPal, and Mollie against the shipped payment capabilities.
    2. Configure credentials, HTTP ownership, client handoffs, and provider-specific limits.
    3. Add optional authenticated notifications, unsigned Mollie hints, or local call observations.
    On this page

    Choose Memory for local simulation, Stripe for hosted or native card payments, PayPal for wallet Orders, or Mollie for hosted one-off card payments. Stripe and PayPal also enroll buyers in existing recurring plans. Changing drivers does not migrate resources, credentials, or customer agreements.

    Install core from your application’s module directory:

    go get arkoren.dev/core

    The fragments use arkoren.dev/core/service/payment. Load credentials from server configuration, check construction errors, and begin with test/sandbox credentials. None of these drivers needs lifecycle hooks, a worker, or a webhook to make outbound calls. Construction performs no network I/O.

    Compatibility Matrix

    This compares Arkoren’s shipped operations and companions, not each vendor’s full product. Memory supplies local state, not a real buyer page or collection.

    Driver Hosted buyer approval Merchant completion Payment lookup Refund creation and lookup Subscription enrollment Immediate cancellation Client preparation Authenticated notifications Facade call observations
    Memory No
    Stripe Yes Yes Yes Yes
    PayPal Yes
    Mollie Yes No No No

    Approval is not collection, refund acceptance is not returned funds, and an active subscription is not paid access. Cancellation is immediate, without proration or refunds. Retain durable operation keys and respect provider-specific recovery limits before replaying any mutation.

    Memory

    Use the bundled simulator when no external financial effect should occur. Each instance has independent state that disappears on exit; it needs no credentials or separate module.

    import (
        "arkoren.dev/core/service/payment"
        "arkoren.dev/core/service/payment/memorypayment"
    )
    
    simulator := memorypayment.New()
    payments := payment.New(simulator)

    There are no optional settings or native client. Use the local payment journey to supply approval and settlement explicitly. Completion creates a pending payment; advancing time does not settle it or collect a renewal.

    Preparation creates a pending payment and inert memory.authorization token, not a checkout or SDK credential. ResolvePayment supplies its result. ResolveSubscription separately supplies subscription lifecycle, not invoice payment. For ambiguous replies rather than simulated state, use the scripted fake.

    See the memorypayment API.

    Stripe

    Choose Stripe for hosted Checkout or guest native card intents. Obtain a Stripe test-mode API key; recurring enrollment additionally needs an existing recurring Price. One-time checkout uses an inline amount rather than a precreated Price.

    go get arkoren.dev/driver/stripe

    Connecting Stripe

    With stripeKey loaded from server configuration, create the backend and facade:

    import "arkoren.dev/driver/stripe/stripepayment"
    
    backend, err := stripepayment.New(stripeKey)
    payments := payment.New(backend)

    The key must be nonempty without whitespace or controls. Invalid keys return payment.ErrInvalidRequest before callbacks run. Valid-key callbacks run once in order; nil callbacks and callback panics propagate. HTTP settings are checked afterwards.

    To use a shorter timeout, construct the backend with net/http and time:

    backend, err := stripepayment.New(stripeKey, func(connection *stripepayment.Builder) {
        connection.HTTPClient(&http.Client{Timeout: 8 * time.Second})
    })

    The default/zero timeout is 30 seconds; a negative timeout fails construction. New copies HTTP settings, refuses redirects, and disables SDK retries and standard HTTP mutation replays, logging, and telemetry. Borrowed transports stay caller-owned and must not independently replay mutations.

    NewFrom instead preserves the supplied SDK client’s retry, redirect, logging, and timeout policies. Client() exposes the retained native client; direct native operations and their safety remain yours. The driver closes neither client nor transport. Do not mutate shared clients during requests.

    Payment Flows

    Follow Hosted Checkout for a redirect or Recurring Subscriptions with a recurring Price at quantity one. Stripe’s hosted UI collects payment; CompleteCheckout only observes completion and never charges again. Checkout Session, PaymentIntent, Refund, and Subscription IDs retain their distinct native families.

    For Mobile Payments, PreparePayment creates an unconfirmed, card-only PaymentIntent with automatic confirmation/capture settings. The sensitive stripe.payment_intent authorization goes only to the authenticated owner. Empty, web, iOS, and Android channels use the same guest profile. ReturnURL and DeviceReference must be empty; configure client return routing in the SDK. No Customer, CustomerSession, ephemeral key, or saved-card setup is supplied.

    Refunds perform a currency-checking PaymentIntent preflight, then submit the explicit refund without post-success hydration. Immediate subscription cancellation requests neither proration nor a new final invoice. NextBillingAt stays absent: item period boundaries do not establish another collection. Dispatched ambiguous mutations can wrap ErrOutcomeUnknown; preserve the original request/key even if a later call fails differently.

    Stripe Notifications

    Notifications are optional and use stripewebhook, installed by the same vendor module, independently of the payment backend. Configure a public HTTPS endpoint in Stripe with snapshot event version stripe.APIVersion from github.com/stripe/stripe-go/v87. Its endpoint signing secret is not the API key.

    The fragments assume r *router.Router and an application-owned accept webhook.AcceptFunc[payment.Observation] that durably accepts observations and returns its error. Import arkoren.dev/core/http/webhook and construct the source first:

    import "arkoren.dev/driver/stripe/stripewebhook"
    
    source, err := stripewebhook.New(endpointSecret)
    if err != nil {
        return err
    }

    Bind only needed events and mount the checked handler:

    handler, err := webhook.Handler(source, func(receiver *webhook.Builder[stripewebhook.Envelope]) {
        receiver.Handle(stripewebhook.PaymentIntentSucceeded, accept)
    })
    if err != nil {
        return err
    }
    r.Post("/payments/stripe/events", handler)

    Verification uses original bytes and timestamped signatures without network I/O. The secret needs whsec_, a nonempty suffix, and no whitespace/controls before callbacks run. Signature tolerance defaults to five minutes; change it with stripewebhook.Builder.Tolerance. Zero restores the default; negative is invalid. Freshness is not durable replay prevention.

    Bound snapshot version/schema mismatches fail decoding retryably; they are not authentication failures. Thin events and fetching missing relationships are not supported. Envelope.CreatedAt is native Unix seconds (zero means absent), not time.Time; JSON still uses created. Positive values populate portable Observation.OccurredAt.

    Definitions also cover Checkout completion, intent failure, refund updates, subscription creation/update/deletion, and invoice paid/payment-failed/ invoice-payment-paid. Checkout/subscription lifecycle is not collection evidence; invoice.paid can describe zero or externally satisfied invoices. Correlate invoice references without guessing missing relationships. Events can repeat or arrive out of order, and refunds can remain pending.

    Apply the receiver acceptance rules below. See the Stripe payment API and Stripe webhook API.

    PayPal

    Choose PayPal for hosted or native wallet Orders. Create a developer application and use sandbox merchant credentials. Subscription enrollment additionally needs an existing account Plan; a Stripe Price is not interchangeable.

    go get arkoren.dev/driver/paypal

    Connecting PayPal

    Load the sandbox application’s credentials from server configuration:

    import "arkoren.dev/driver/paypal/paypalpayment"
    
    backend, err := paypalpayment.New(merchantID, merchantSecret)
    payments := payment.New(backend)

    Callbacks run once in order before nonblank credential validation; nil callbacks panic. Sandbox is the default. To bound requests, use net/http and time:

    backend, err := paypalpayment.New(merchantID, merchantSecret, func(merchant *paypalpayment.Builder) {
        merchant.HTTPClient(&http.Client{Timeout: 8 * time.Second})
    })

    HTTP settings are copied, redirects refused, and a nonpositive timeout becomes 30 seconds. Transports/jars remain borrowed and are never closed. Fixed trusted API origins and a private token cache require no refresh worker. Requests, including failed mutations, are not automatically replayed; custom transports must preserve that boundary. HTTPClient() exposes the actual client, not an SDK; treat it as read-only during use.

    Only after sandbox verification, use production credentials and merchant.Live(true) in construction. That setting selects a live origin and can move real money; merchant credentials, resources, and environment must match.

    Payment Flows

    Use Hosted Checkout and explicitly complete an approved Order with a separately persisted completion key. An approved Order can expire without capture; neither a notification decoder nor a return link captures it. Coordinate the same completion operation across authorized POSTs and workers.

    For Mobile Payments, preparation returns a native paypal.order action and a CheckoutID, not a financial PaymentID. Empty, iOS, and Android channels work; web and nonempty ReturnURL are unsupported. No backend approval link is required. Optional DeviceReference carries at most 255 visible ASCII bytes as PayPal-Client-Metadata-Id on creation only. It is risk correlation, never authorization. Client SDK approval must still precede server completion.

    Preserve returned resource-family prefixes. Payment reads/refunds support one-time Payments v2 captures, not subscription sales. Sale observations do not enable deprecated sale lookup/refund; those operations return ErrUnsupported. PayPal supports a subset of core currencies. HUF amounts must represent whole forints using canonical two-decimal minor units; unsupported currencies and fractional forints fail before authentication/payment I/O.

    Subscriptions activate through buyer approval. Cancellation supports active/suspended subscriptions; already-canceled succeeds without a mutation. There is no period-end scheduling, plan change, or proration. Provider-supplied NextBillingAt and active status do not establish payment.

    Malformed successful GET JSON wraps ErrUnavailable without outcome uncertainty; semantic snapshot failures retain their specific validation/capability errors. Unusable OAuth responses also wrap ErrUnavailable before business dispatch. Neither resolves an earlier uncertain mutation. Successful mutation decoding failures remain outcome-unknown. paypalpayment.Error exposes safe status and machine-token fields, but unwrapped causes still require redaction.

    PayPal Notifications

    Use the separate paypalwebhook package from the same module. Configure a public HTTPS webhook endpoint and selected event types in the matching sandbox account. The webhook registration ID and merchant credentials must share its environment. Use r *router.Router, import arkoren.dev/core/http/webhook, and supply an application-owned accept webhook.AcceptFunc[payment.Observation] that durably accepts observations and returns its error:

    import "arkoren.dev/driver/paypal/paypalwebhook"
    
    source, err := paypalwebhook.New(merchantID, merchantSecret, registrationID)
    if err != nil {
        return err
    }

    Bind capture completion when your application needs collection observations:

    handler, err := webhook.Handler(source, func(receiver *webhook.Builder[paypalwebhook.Envelope]) {
        receiver.Handle(paypalwebhook.PaymentCaptureCompleted, accept)
    })
    if err != nil {
        return err
    }
    r.Post("/payments/paypal/events", handler)

    Production is selected separately with paypalwebhook.Builder.Live(true) and matching production registration/credentials. Its HTTPClient setting copies client settings, refuses redirects, and defaults nonpositive timeouts to 30 seconds; its transport stays caller-owned.

    The source verifies original bytes remotely at a fixed PayPal origin using the request context. It never fetches arbitrary certificate URLs or enriches events. Verifier outages are retryable, not invalid signatures. Authentication precedes duplicate suppression, so repeated events can cause verification requests. Delivery identity is the event ID, not a transmission attempt ID.

    Other definitions cover Order approval, capture outcomes, sale collection/refund, refund progress, and subscription lifecycle. Binding CheckoutOrderApproved does not capture: your authorized worker calls CompleteCheckout with the saved completion key. Missing relationships stay absent; events can repeat or arrive out of order. Mock events do not prove signature verification; validate actual sandbox deliveries separately.

    Apply the receiver acceptance rules. See the PayPal payment API and PayPal webhook API.

    Mollie

    Choose Mollie for hosted, one-off creditcard payments with automatic capture and explicit refunds. Create a website profile, enable that method, and obtain a test profile API key. Other methods, manual capture, customer/mandate setup, recurring APIs, and native SDK preparation are not exposed.

    go get arkoren.dev/driver/mollie

    Connecting Mollie

    With the profile key loaded from server configuration:

    import "arkoren.dev/driver/mollie/molliepayment"
    
    backend, err := molliepayment.New(profileKey)
    payments := payment.New(backend)

    Keys need a test_ or live_ prefix and alphanumeric suffix. Their prefix selects mode at the fixed API origin; OAuth/organization credentials are unsupported. A live key can move money. Callbacks run once in order, even for invalid keys; nil callbacks panic.

    Callbacks are disabled by default. To receive status hints, configure a reachable absolute HTTPS URL without credentials/fragments, using net/http and time:

    backend, err := molliepayment.New(profileKey, func(merchant *molliepayment.Builder) {
        merchant.WebhookURL("https://shop.example/payments/mollie")
        merchant.HTTPClient(&http.Client{Timeout: 8 * time.Second})
    })

    This installs no receiver or authentication. Nonpositive/omitted timeouts become 30 seconds. HTTP settings are copied, redirects refused, and borrowed transport/jar objects never closed. HTTPClient() exposes the actual client; do not mutate it during use or weaken redirect policy. Custom transports must neither replay mutations nor log credentials.

    Payment Flows

    Use Hosted Checkout. The driver fixes method: creditcard, sequenceType: oneoff, and captureMode: automatic. An empty description becomes Payment; Reference becomes correlation metadata, not idempotency. Checkout/payment IDs refer to the same tr_ resource using different Go ID types. NeedsCompletion is false and explicit completion returns ErrUnsupported. Only native paid maps to succeeded; authorized and pending are not collection.

    Use Payment Refunds with the original currency. Mollie enforces currency and remaining balance without hidden preflight or post-mutation reads. Save the entire compound refund handle: it includes the parent payment needed for lookup. Queued/pending/processing refunds remain pending; only refunded succeeds, and unfamiliar native states stay unknown.

    Subscriptions and preparation validate portable input, then return ErrUnsupported without HTTP. This does not disable hosted one-off checkout. Mollie Components, native wallets, and Mollie’s recurring product are not shipped capabilities of this driver.

    Mollie caches POST responses for one hour per exact API credential. Save the request, key, and credential identity before dispatch. Credential rotation or replay after expiry can create another effect, even within the same account. See Payment Recovery before replaying.

    There is one mutation attempt without automatic retries. Transport loss, conflicts, server failures, redirects, and unusable mutation successes can wrap ErrOutcomeUnknown. Malformed/unusable successful GET snapshots instead wrap ErrUnavailable, including missing required amounts or mismatched identities; transport/non-2xx failures retain their categories. None clears earlier uncertainty.

    molliepayment.Error.Status retains a non-2xx status or zero if none was retained. Zero can follow a successful HTTP reply that could not be read/decoded; it does not prove no reply arrived. Errors omit provider bodies. Driver fmt/slog formatting yields Driver{REDACTED}, hiding owned credentials and callback URL, but borrowed HTTP objects and explicitly unwrapped causes remain your redaction responsibility.

    Mollie Callbacks

    Classic callbacks are unsigned form POSTs with an id hint. Anyone can forge them; there is no shipped molliewebhook or authenticated Source. Rate-limit and bound the route, accept exactly one ID, check its persisted merchant/test-live association, and read authoritative provider state with that merchant’s client.

    With mux *http.ServeMux and your application handler, register a plain route:

    mux.HandleFunc("POST /payments/mollie", handleMollie)

    Mollie Callbacks shows parsing, ownership, lookup, and durable acceptance. Unknown IDs receive an acknowledgement without order disclosure. Require succeeded state and matching order amount/currency before fulfillment; duplicate hints must not duplicate effects. Refund callbacks carry the payment ID, so reconcile the compound refund handles you already saved. Next-gen webhooks are outside scope.

    See the Mollie payment API.

    Notification Acceptance

    Stripe/PayPal receivers use the HTTP webhook receiver. By default, it answers 200 after acceptance, including unbound authenticated types; 503 for decode/accept errors; 400 for rejected authentication; and 413 for an oversized body. The receiver commits its failure response before returning an error. Receipt 4xx errors match webhook.ErrRejected; 503 errors match webhook.ErrUnavailable, retaining causes. An outer error mapper cannot rewrite that response, and unwrapped causes require redaction.

    Accept observations durably before acknowledging. A 200 does not establish asynchronous completion or successful collection. webhook.Delivery carries native event identity; payment.Observation carries facts/time. Use Event.Map for application queue DTOs. Scope deduplication by provider, merchant, environment, and consumer; cache deduplication replaces neither a durable inbox/outbox nor operation keys.

    Local Monitoring

    To observe facade calls locally, supply an in-process event bus at construction. With driver payment.Driver and bus *event.Bus already configured:

    payments := payment.New(driver, func(client *payment.Builder) {
        client.Events(bus)
    })

    Register listeners on payment.OperationCompleted and OperationFailed using the event bus. Their payment.OperationEvent contains only a fixed operation name and bounded ErrorType, not IDs, amounts, URLs, keys, secrets, or arbitrary error text. Omitted/nil Events disables observations; direct driver calls bypass them.

    Each driver-returned call emits once: nil error completes, non-nil error fails. Validation failures, missing drivers, and pre-canceled calls emit nothing. payment.ErrOutcomeUnknown takes precedence over joined error classes, while the returned error retains its causes. Listeners run synchronously after telemetry with the operation context; their errors/panics cannot alter the API result, but slow listeners delay return.

    These best-effort observations include reads and replays. They prove neither money movement nor durable reconciliation and must not trigger fulfillment. Provider notifications and application-owned operation records remain separate. For facade configuration, see the payment API.

    Open a bulk documentation archive?

    This file contains every guide in its scope, not API declarations. Loading it all into an agent can exhaust its context window and increase processing time and cost. For coding tasks, start with the index and read only the relevant pages.

    Use the full archive for offline indexing, bulk processing, or deliberate large-context use.

    Browse the index instead
    Open archive