Skip to content

Repository files navigation

saweraspay

Go Reference

Unofficial Go client for Saweria — Indonesia's tip / donation platform.

It wraps Saweria's internal donation endpoints so you can integrate Saweria into your own application as a lightweight payment backend (QRIS, GoPay, DANA), and ships a standalone webhook server you can run to receive payment notifications.

Disclaimer: This library targets Saweria's undocumented backend. It is not affiliated with or endorsed by Saweria. Endpoints may break without notice. Test thoroughly before using in production.

Features

  • ✅ Calculate payment-gateway fees per method (QRIS / GoPay / DANA)
  • ✅ Create pending donation transactions and obtain payment instruments (QR string, redirect URL, DANA mobile/web actions)
  • ✅ Parse Saweria webhook callbacks
  • ✅ Configurable username + user ID (one client per Saweria account, multiple clients OK)
  • ✅ Production-ready webhook server with idempotency, slog logging, graceful shutdown, body size limit
  • ✅ 100% standard library — no external dependencies

Install

go get github.com/sholehbaktiabadi/saweraspay

Requires Go 1.22+.

Configuration

You need two values from your Saweria account:

Field Example Where to find it
username your-saweria-username The slug in your Saweria URL (saweria.co/your-saweria-username)
userID 595ace77-9e88-493b-acae-e9752b0cd829 Open DevTools on your Saweria donation page, trigger a donation, and inspect the request to /donations/snap/{uuid}

Quick start

1. Calculate the payment-gateway fee

Display the final amount to the donor before they confirm:

package main

import (
    "context"
    "fmt"

    "github.com/sholehbaktiabadi/saweraspay"
)

func main() {
    client := saweraspay.NewClient(
        "your-saweria-username",
        "595ace77-9e88-493b-acae-e9752b0cd829",
    )

    resp, err := client.CalculatePayment(context.Background(), saweraspay.CalculatePaymentRequest{
        Message:     "INV-12345",
        Amount:      5000,
        PaymentType: saweraspay.PaymentTypeQRIS,
        Customer: saweraspay.CustomerInfo{
            FirstName: "John",
            Email:     "john@example.com",
            Phone:     "081234567890",
        },
    })
    if err != nil {
        panic(err)
    }

    fmt.Printf("Donor will pay %d IDR (fee: %d)\n", resp.AmountToPay, resp.PgFee)
}

2. Create a pending payment

resp, err := client.GeneratePayment(ctx, saweraspay.GeneratePaymentRequest{
    Message:     "INV-12345",   // YOUR reference id — echoed back in the webhook
    Amount:      5000,
    PaymentType: saweraspay.PaymentTypeQRIS,
    Customer:    customer,
})

The response shape depends on the payment method:

Payment type Use field Description
qris resp.QRString EMV QRIS payload — render as a QR code
gopay resp.RedirectURL Midtrans Snap URL — redirect donor's browser
dana resp.RedirectURL or resp.Actions Use Actions[].URLType == "MOBILE" for in-app, "WEB" for browser
switch resp.PaymentType {
case saweraspay.PaymentTypeQRIS:
    renderQR(resp.QRString) // your QR rendering
case saweraspay.PaymentTypeGoPay:
    http.Redirect(w, r, resp.RedirectURL, http.StatusFound)
case saweraspay.PaymentTypeDana:
    // mobile flow: prefer MOBILE action; fall back to RedirectURL
    for _, a := range resp.Actions {
        if a.URLType == "MOBILE" {
            return a.URL
        }
    }
    return resp.RedirectURL
}

Important: persist resp.ID and req.Message together in your DB. When the webhook fires, you match by Message to find the originating order.

3. Receive payment notifications (webhook)

Saweria POSTs a JSON payload to your callback URL on successful payment. Configure the URL in your Saweria dashboard.

Use the library inside your own server

http.HandleFunc("POST /webhook", func(w http.ResponseWriter, r *http.Request) {
    payload, err := saweraspay.ParseWebhook(r.Body)
    if err != nil {
        http.Error(w, "bad payload", http.StatusBadRequest)
        return
    }

    // Match payload.Message back to your order, mark it paid.
    order := lookupOrder(payload.Message)
    order.MarkPaid(payload.AmountRaw, payload.ID)

    w.WriteHeader(http.StatusOK)
})

Or run the bundled webhook server

The cmd/webhook-server binary is a production-ready receiver that handles parsing, idempotency, logging, and graceful shutdown. Use it as a starting point — extend cmd/webhook-server/handler.go to call into your business logic.

# install
go install github.com/sholehbaktiabadi/saweraspay/cmd/webhook-server@latest

# run with defaults (listens on :8080/webhook)
webhook-server

# run with a secret path (recommended — see Security)
WEBHOOK_PATH=/webhook/$(openssl rand -hex 16) webhook-server

Webhook server environment variables

Variable Default Description
ADDR :8080 Listen address
WEBHOOK_PATH /webhook URL path — embed a secret token here (see Security)
MAX_BODY_BYTES 65536 Request body size limit
LOG_LEVEL info debug, info, warn, error (JSON output to stdout)

Security

Saweria does not sign webhook payloads. Anyone who knows your webhook URL can forge a "payment success" event and trick your app into marking orders as paid. You MUST mitigate this:

  1. Use a long secret in the URL path — treat the path itself as a shared secret.

    WEBHOOK_PATH=/webhook/$(openssl rand -hex 32) webhook-server

    Register this exact URL in Saweria. Rotate periodically.

  2. Always terminate TLS — run behind nginx, caddy, Cloudflare, or a load balancer. Never expose the webhook server over plain HTTP in production.

  3. (Optional) Restrict by source IP — if you can identify Saweria's outbound IP range, allowlist it at the firewall.

  4. Verify idempotency at the storage layer — the bundled handler dedupes by payload.ID in memory; this resets on restart and won't dedupe across replicas. Persist processed IDs in Redis / a DB if you run more than one replica or care about replay attacks across restarts.

Caveats and known limitations

  • The webhook payload does not include payment_type. Only QRIS is detectable (via the presence of qr_string). For GoPay/DANA, look up the method by Message in your DB.
  • The webhook also does not include a status field — receiving the webhook means the payment succeeded.
  • Saweria's donator.phone is sometimes null in responses even when supplied in the request.
  • This wrapper currently exposes only calculate_pg_amount and donations/snap (create). A status-check endpoint is not exposed; rely on the webhook for state.

Library reference

Types

type Client struct{ /* ... */ }

func NewClient(username, userID string, opts ...Option) *Client

type Option func(*Client)
func WithBaseURL(url string) Option        // override the Saweria backend host
func WithHTTPClient(h *http.Client) Option // custom transport / timeouts
func WithUserAgent(ua string) Option       // override User-Agent

type PaymentType string
const (
    PaymentTypeQRIS  PaymentType = "qris"
    PaymentTypeGoPay PaymentType = "gopay"
    PaymentTypeDana  PaymentType = "dana"
)

Methods

func (c *Client) CalculatePayment(ctx, req CalculatePaymentRequest) (*CalculatePaymentResponse, error)
func (c *Client) GeneratePayment(ctx, req GeneratePaymentRequest) (*GeneratePaymentResponse, error)
func ParseWebhook(r io.Reader) (*WebhookPayload, error)

Errors

All validation errors are sentinel values — use errors.Is:

errors.Is(err, saweraspay.ErrEmptyMessage)
errors.Is(err, saweraspay.ErrInvalidAmount)
errors.Is(err, saweraspay.ErrInvalidPayment)
errors.Is(err, saweraspay.ErrEmptyEmail)
errors.Is(err, saweraspay.ErrEmptyFirstName)
errors.Is(err, saweraspay.ErrMissingUsername)
errors.Is(err, saweraspay.ErrMissingUserID)

HTTP errors from Saweria are returned as *APIError:

var apiErr *saweraspay.APIError
if errors.As(err, &apiErr) {
    log.Printf("saweria returned %d: %s", apiErr.StatusCode, apiErr.Body)
}

Development

go test ./...     # run tests
go vet ./...      # static checks
go build ./...    # build everything (including cmd/webhook-server)

The repo has no third-party dependencies; tests use the standard net/http/httptest.

Contributing

Issues and PRs welcome. If Saweria changes their backend and an endpoint breaks, please open an issue with a curl reproduction.

License

MIT (suggested) — add a LICENSE file to publish.

About

Saweraspay is a lightweight Go library for integrating with the Saweria Payment Gateway API.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages