qint

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 8, 2026 License: MIT Imports: 13 Imported by: 0

README

qint-go

Official Go client for the Qint merchant API — a thin, typed wrapper for creating payment intents, reading them back, and verifying webhooks.

  • Zero third-party dependencies (standard library only)
  • context.Context on every request
  • Typed models, typed errors, and constant-time webhook verification
  • Go 1.21+

Install

go get github.com/SwizzX-GmbH/qint-go

The package is not yet published to a language package registry — Go doesn't use one. Modules are served straight from Git, so the command above works today against this repo. See PUBLISH.md for how versions are released (tag-based).

import qint "github.com/SwizzX-GmbH/qint-go"

Quickstart (30 seconds)

package main

import (
	"context"
	"fmt"
	"log"

	qint "github.com/SwizzX-GmbH/qint-go"
)

func main() {
	client := qint.NewClient("qk_live_...")

	// 1. Create a payment intent.
	intent, err := client.CreateIntent(context.Background(), qint.CreateIntentParams{
		Amount:    19.90,
		Currency:  qint.CurrencyCHF,
		Title:     "Order #1024",
		ReturnURL: "https://shop.example/thanks",
	})
	if err != nil {
		log.Fatal(err)
	}

	// 2. Redirect the buyer to the hosted checkout.
	fmt.Println("Send the buyer to:", intent.CheckoutURL)

	// 3. Learn the outcome by polling GetIntent (below) or, preferably, by
	//    receiving a webhook (see "Webhooks").
	latest, err := client.GetIntent(context.Background(), intent.ID)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("Status:", latest.Status)
}
Configuration
client := qint.NewClient("qk_live_...",
	qint.WithBaseURL("https://qint-api.fly.dev/api/v1"), // default; api.qint.ch is coming
	qint.WithTimeout(15*time.Second),                    // default 30s
	// qint.WithHTTPClient(myClient),                    // full control over transport
)

The client sends Authorization: Bearer <apiKey> on every request.

API

Method Description Scope
CreateIntent(ctx, CreateIntentParams) (*Intent, error) Create a payment intent Write
GetIntent(ctx, id string) (*Intent, error) Fetch one intent by id Read
ListIntents(ctx, ListIntentsParams) (*IntentList, error) List intents (paged, newest first) Read
type CreateIntentParams struct {
	Amount         float64  // required, decimal, e.g. 19.90
	Currency       Currency // required: CurrencyCHF | CurrencyEUR | CurrencyUSD
	Title          string   // optional
	IdempotencyKey string   // optional — replays instead of duplicating on retry
	ReturnURL      string   // optional https URL, <=500 chars
}

type ListIntentsParams struct {
	Status   IntentStatus // optional filter
	Page     int          // optional (default 1)
	PageSize int          // optional (default 20, max 100)
}

Intent statuses are the seven lowercase values: initiated, pending, confirmed, settled, failed, expired, cancelled (exported as StatusInitiated, StatusPending, ... StatusCancelled).

Intent.Amount and PaymentStatusEvent.Amount are decoded as json.Number to preserve the exact decimal the server sent — use .String() or .Float64().

Errors

Any non-2xx response is returned as a typed *QintError carrying the HTTP status and the API's RFC 7807 problem-details detail message:

intent, err := client.CreateIntent(ctx, params)
if err != nil {
	var qerr *qint.QintError
	if errors.As(err, &qerr) {
		// qerr.StatusCode, qerr.Title, qerr.Detail
		if qerr.StatusCode == 403 {
			log.Fatalf("missing scope: %s", qerr.Detail)
		}
	}
	log.Fatal(err)
}

Webhooks

Configure an endpoint in the Qint dashboard to receive a signing secret (whsec_...). Each delivery is a JSON POST with headers:

  • X-Qint-Signature: sha256=<hex HMAC-SHA256 of the raw body>
  • X-Qint-Event-Id: <id> — use it to deduplicate re-deliveries

Always verify the signature against the exact raw request body bytes before trusting the payload, and acknowledge quickly with a 2xx.

func handleQintWebhook(signingSecret string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		body, err := io.ReadAll(r.Body)
		if err != nil {
			w.WriteHeader(http.StatusBadRequest)
			return
		}

		// Constant-time HMAC-SHA256 verification.
		if !qint.VerifyWebhookSignature(body, r.Header.Get("X-Qint-Signature"), signingSecret) {
			w.WriteHeader(http.StatusUnauthorized)
			return
		}

		event, err := qint.ParseEvent(body)
		if err != nil {
			w.WriteHeader(http.StatusBadRequest)
			return
		}

		// Dedupe on the event id before doing any work.
		eventID := r.Header.Get("X-Qint-Event-Id")
		if alreadyProcessed(eventID) {
			w.WriteHeader(http.StatusOK)
			return
		}

		switch event.Status {
		case qint.StatusSettled:
			// fulfill the order for event.IntentID
		case qint.StatusFailed, qint.StatusExpired, qint.StatusCancelled:
			// release / cancel
		}

		w.WriteHeader(http.StatusOK) // ack fast
	}
}

PaymentStatusEvent fields: Type ("payment.status"), IntentID, Status, Amount, Currency, AssetSymbol, InvoiceID?, PaymentLinkID?, OccurredAt, Underpaid?, ExpectedCryptoAmount?, ReceivedCryptoAmount?.

Development

go test ./...      # runs fully offline (httptest + stdlib)
go vet ./...
gofmt -l .

License

MIT © SwizzX GmbH

Documentation

Overview

Package qint is the official Go client for the Qint merchant API.

It is a thin, typed wrapper over the Qint public HTTP API: create payment intents, look them up, list them, and verify incoming webhook deliveries.

Quickstart

client := qint.NewClient("qk_live_...")

intent, err := client.CreateIntent(ctx, qint.CreateIntentParams{
	Amount:    19.90,
	Currency:  qint.CurrencyCHF,
	Title:     "Order #1024",
	ReturnURL: "https://shop.example/thanks",
})
if err != nil {
	// non-2xx responses are returned as *qint.QintError
}
// Redirect the buyer to intent.CheckoutURL to complete payment.

The default base URL is https://qint-api.fly.dev/api/v1 and can be overridden with WithBaseURL (an api.qint.ch host is planned).

See https://docs.qint.ch for the full API reference.

Example

Example shows the 30-second flow: create an intent, then send the buyer to the hosted checkout.

package main

import (
	"context"
	"fmt"
	"log"

	qint "github.com/SwizzX-GmbH/qint-go"
)

func main() {
	client := qint.NewClient("qk_live_...")

	intent, err := client.CreateIntent(context.Background(), qint.CreateIntentParams{
		Amount:    19.90,
		Currency:  qint.CurrencyCHF,
		Title:     "Order #1024",
		ReturnURL: "https://shop.example/thanks",
	})
	if err != nil {
		log.Fatal(err)
	}

	// Redirect the buyer to the hosted checkout to complete payment.
	fmt.Println(intent.CheckoutURL)
}

Index

Examples

Constants

View Source
const DefaultBaseURL = "https://qint-api.fly.dev/api/v1"

DefaultBaseURL is the production Qint merchant API endpoint. A dedicated api.qint.ch host is planned; override it with WithBaseURL when it lands.

View Source
const Version = "0.1.0"

Version is the semantic version of this SDK.

Variables

This section is empty.

Functions

func VerifyWebhookSignature

func VerifyWebhookSignature(rawBody []byte, signatureHeader, secret string) bool

VerifyWebhookSignature reports whether signatureHeader is a valid HMAC-SHA256 signature of rawBody produced with the endpoint's signing secret (whsec_...). signatureHeader is the raw X-Qint-Signature header value, i.e. "sha256=<hex>"; a bare "<hex>" is also accepted. The comparison is constant-time (crypto/hmac.Equal).

Always verify against the exact raw request body bytes — before any JSON decoding — and reject the delivery if this returns false. Use the X-Qint-Event-Id header to deduplicate re-deliveries of the same event.

Example (Handler)

ExampleVerifyWebhookSignature_handler verifies an incoming webhook against the raw request body, then parses it. Deduplicate on the X-Qint-Event-Id header and always acknowledge quickly with a 2xx.

package main

import (
	"io"
	"net/http"

	qint "github.com/SwizzX-GmbH/qint-go"
)

func main() {
	const signingSecret = "whsec_..." // from the Qint dashboard

	http.HandleFunc("/webhooks/qint", func(w http.ResponseWriter, r *http.Request) {
		body, err := io.ReadAll(r.Body)
		if err != nil {
			w.WriteHeader(http.StatusBadRequest)
			return
		}

		if !qint.VerifyWebhookSignature(body, r.Header.Get("X-Qint-Signature"), signingSecret) {
			w.WriteHeader(http.StatusUnauthorized)
			return
		}

		event, err := qint.ParseEvent(body)
		if err != nil {
			w.WriteHeader(http.StatusBadRequest)
			return
		}

		eventID := r.Header.Get("X-Qint-Event-Id")
		// TODO: skip if eventID was already processed (dedupe), then handle
		// event.Status for event.IntentID.
		_ = eventID
		_ = event

		w.WriteHeader(http.StatusOK) // ack fast
	})
}

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is a typed client for the Qint merchant API. A Client is safe for concurrent use by multiple goroutines. Construct one with NewClient.

func NewClient

func NewClient(apiKey string, opts ...Option) *Client

NewClient returns a Client authenticated with the given API key (qk_live_...). Every request carries "Authorization: Bearer <apiKey>".

func (*Client) CreateIntent

func (c *Client) CreateIntent(ctx context.Context, params CreateIntentParams) (*Intent, error)

CreateIntent creates a new payment intent and returns it. Send the buyer to the returned Intent.CheckoutURL to complete payment. Requires an API key with the Write scope.

func (*Client) GetIntent

func (c *Client) GetIntent(ctx context.Context, id string) (*Intent, error)

GetIntent fetches a single payment intent by its id (pi_...). Requires the Read scope.

func (*Client) ListIntents

func (c *Client) ListIntents(ctx context.Context, params ListIntentsParams) (*IntentList, error)

ListIntents returns one page of intents, most recent first. Requires the Read scope.

type CreateIntentParams

type CreateIntentParams struct {
	// Amount is the fiat amount to charge as a decimal, e.g. 19.90.
	Amount float64 `json:"amount"`
	// Currency is the settlement currency (CurrencyCHF, CurrencyEUR or
	// CurrencyUSD).
	Currency Currency `json:"currency"`
	// Title is an optional human-readable description shown at checkout.
	Title string `json:"title,omitempty"`
	// IdempotencyKey, when set, makes CreateIntent safe to retry: the API
	// replays the original intent (HTTP 200) instead of creating a duplicate.
	IdempotencyKey string `json:"idempotencyKey,omitempty"`
	// ReturnURL is an optional https URL (max 500 chars) the buyer is sent
	// back to after checkout.
	ReturnURL string `json:"returnUrl,omitempty"`
}

CreateIntentParams are the inputs to CreateIntent. Amount and Currency are required; the remaining fields are optional.

type Currency

type Currency string

Currency is a fiat settlement currency supported by Qint.

const (
	CurrencyCHF Currency = "CHF"
	CurrencyEUR Currency = "EUR"
	CurrencyUSD Currency = "USD"
)

Supported settlement currencies.

type Intent

type Intent struct {
	ID             string       `json:"id"`
	Status         IntentStatus `json:"status"`
	Amount         json.Number  `json:"amount"`
	Currency       Currency     `json:"currency"`
	Title          string       `json:"title,omitempty"`
	AssetSymbol    string       `json:"assetSymbol,omitempty"`
	CryptoAmount   string       `json:"cryptoAmount,omitempty"`
	DepositAddress string       `json:"depositAddress,omitempty"`
	CheckoutURL    string       `json:"checkoutUrl"`
	ReturnURL      string       `json:"returnUrl,omitempty"`
	CreatedAt      time.Time    `json:"createdAt"`
	ExpiresAt      time.Time    `json:"expiresAt"`
	ConfirmedAt    *time.Time   `json:"confirmedAt,omitempty"`
	SettledAt      *time.Time   `json:"settledAt,omitempty"`
}

Intent is a Qint payment intent.

Amount is decoded as json.Number to preserve the exact decimal value the API returned (call Amount.String() or Amount.Float64()). CryptoAmount is an 8-decimal-place string. Optional timestamps are pointers that are nil until the intent reaches the corresponding state.

type IntentList

type IntentList struct {
	Items    []Intent `json:"items"`
	Total    int      `json:"total"`
	Page     int      `json:"page"`
	PageSize int      `json:"pageSize"`
}

IntentList is one page of intents returned by ListIntents.

type IntentStatus

type IntentStatus string

IntentStatus is the lifecycle status of a payment intent. The API always returns lowercase values.

const (
	StatusInitiated IntentStatus = "initiated"
	StatusPending   IntentStatus = "pending"
	StatusConfirmed IntentStatus = "confirmed"
	StatusSettled   IntentStatus = "settled"
	StatusFailed    IntentStatus = "failed"
	StatusExpired   IntentStatus = "expired"
	StatusCancelled IntentStatus = "cancelled"
)

The seven payment-intent statuses.

type ListIntentsParams

type ListIntentsParams struct {
	// Status, when set, filters to a single status.
	Status IntentStatus
	// Page is the 1-based page number (API default 1).
	Page int
	// PageSize is the page size (API default 20, max 100).
	PageSize int
}

ListIntentsParams are the optional filters and pagination for ListIntents. A zero-valued field is omitted from the request, letting the API apply its defaults.

type Option

type Option func(*Client)

Option configures a Client. Pass options to NewClient.

func WithBaseURL

func WithBaseURL(baseURL string) Option

WithBaseURL overrides the API base URL (default DefaultBaseURL). A trailing slash is trimmed.

func WithHTTPClient

func WithHTTPClient(hc *http.Client) Option

WithHTTPClient sets a custom *http.Client, e.g. to configure proxies or a custom transport. A nil client is ignored. Overrides WithTimeout.

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout sets the per-request timeout on the default HTTP client (default 30s). Ignored if WithHTTPClient supplied a client of your own.

type PaymentStatusEvent

type PaymentStatusEvent struct {
	Type                 string       `json:"type"`
	IntentID             string       `json:"intentId"`
	Status               IntentStatus `json:"status"`
	Amount               json.Number  `json:"amount"`
	Currency             Currency     `json:"currency"`
	AssetSymbol          string       `json:"assetSymbol"`
	InvoiceID            string       `json:"invoiceId,omitempty"`
	PaymentLinkID        string       `json:"paymentLinkId,omitempty"`
	OccurredAt           time.Time    `json:"occurredAt"`
	Underpaid            *bool        `json:"underpaid,omitempty"`
	ExpectedCryptoAmount string       `json:"expectedCryptoAmount,omitempty"`
	ReceivedCryptoAmount string       `json:"receivedCryptoAmount,omitempty"`
}

PaymentStatusEvent is the JSON payload delivered to a merchant webhook endpoint. Its Type is always "payment.status".

Amount is decoded as json.Number to preserve the exact decimal value. Underpaid is a pointer so a missing field (nil) is distinguishable from an explicit false.

func ParseEvent

func ParseEvent(rawBody []byte) (*PaymentStatusEvent, error)

ParseEvent unmarshals a webhook request body into a PaymentStatusEvent. Verify the signature with VerifyWebhookSignature first.

type QintError

type QintError struct {
	// StatusCode is the HTTP status code of the response.
	StatusCode int
	// Type is the problem-details "type" URI, if present.
	Type string
	// Title is the short, human-readable summary of the problem type.
	Title string
	// Detail is the human-readable explanation specific to this occurrence.
	Detail string
	// Body is the raw response body, retained for debugging.
	Body string
}

QintError is returned for any non-2xx API response. It carries the HTTP status code together with the RFC 7807 problem-details fields returned by the API. Use errors.As to extract it:

var qerr *qint.QintError
if errors.As(err, &qerr) && qerr.StatusCode == 403 {
	// missing scope, etc.
}

func (*QintError) Error

func (e *QintError) Error() string

Error implements the error interface.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL