# Go library

The `github.com/payitgo/go-sdk` module wraps the payitgo API for Go:
payments, tokens, your webhook (IPN) and its signature check, and the public
checkout calls. It uses only the standard library. Use it on your server only:
it holds your secret key.

Structs carry the API's fields with Go names (`OrderID`, `CheckoutURL`, …) and
the API's JSON tags, so the [Payments](/docs/payments),
[Webhooks](/docs/webhooks) and [Checkout](/docs/checkout) pages describe every
field this library returns.

| Method | HTTP | Returns |
|---|---|---|
| [`client.Payments.Create(ctx, *PaymentParams)`](#paymentscreate) | `POST /v1/payments` | [`*Payment`](#payment) |
| [`client.Payments.Get(ctx, id)`](#paymentsget) | `GET /v1/payments/{id}` | [`*Payment`](#payment) |
| [`client.Payments.List(ctx, *ListParams)`](#paymentslist) | `GET /v1/payments` | [`*PaymentList`](#paymentlist) |
| [`client.Payments.SelectToken(ctx, id, token)`](#paymentsselecttoken) | `POST /v1/payments/{id}/token` | [`*Payment`](#payment) |
| [`client.Payments.Simulate(ctx, id, outcome)`](#paymentssimulate) | `POST /v1/payments/{id}/simulate` | [`*Payment`](#payment) |
| [`client.Tokens.List(ctx, *TokenListParams)`](#tokenslist) | `GET /v1/tokens` | [`[]TokenInfo`](#tokeninfo) |
| [`client.Webhooks.Get(ctx)`](#webhooksget) | `GET /v1/webhook` | [`*Webhook`](#webhook) |
| [`client.Webhooks.Set(ctx, url)`](#webhooksset) | `PUT /v1/webhook` | [`*Webhook`](#webhook) |
| [`client.Webhooks.Remove(ctx)`](#webhooksremove) | `DELETE /v1/webhook` | [`*Webhook`](#webhook) |
| [`client.Webhooks.Test(ctx)`](#webhookstest) | `POST /v1/webhook/test` | [`*WebhookDelivery`](#webhookdelivery) |
| [`client.Webhooks.ConstructEvent(body, header, secret...)`](#webhooksconstructevent) | none (local) | [`*Event`](#event) |
| [`payitgo.ConstructEvent(body, header, secret)`](#constructevent-and-constructeventwithtolerance) | none (local, no client) | [`*Event`](#event) |
| [`payitgo.ConstructEventWithTolerance(body, header, secret, tolerance)`](#constructevent-and-constructeventwithtolerance) | none (local, no client) | [`*Event`](#event) |
| [`client.Checkout.Get(ctx, checkoutToken)`](#checkoutget) | `GET /checkout/{checkout_token}` | [`*Payment`](#payment) |
| [`client.Checkout.SelectToken(ctx, checkoutToken, token)`](#checkoutselecttoken) | `POST /checkout/{checkout_token}/token` | [`*Payment`](#payment) |
| [`client.IsLive(ctx)`](#islive) | `GET /v1/webhook`, the first time only | `bool` |

Every method that calls the API takes a `context.Context` and returns an
`error` last. On error the returned pointer is not nil but holds no data:
check `err` first.

## Install

```bash
go get github.com/payitgo/go-sdk
```

```go
import payitgo "github.com/payitgo/go-sdk"
```

The package is named `payitgo`; import it with that name, since the import path
ends in `go`.

| Requirement | |
|---|---|
| Go | 1.21 or later |
| Dependencies | None (standard library only) |

## Quickstart

```go
package main

import (
	"context"
	"fmt"
	"log"
	"os"

	payitgo "github.com/payitgo/go-sdk"
)

func main() {
	client, err := payitgo.NewClient(os.Getenv("PAYITGO_KEY"), payitgo.WithBaseURL("https://api.payitgo.co"))
	if err != nil {
		log.Fatal(err)
	}
	payment, err := client.Payments.Create(context.Background(), &payitgo.PaymentParams{
		OrderID: "ORD-1001", AmountUSD: "49.90", Token: "USDT_TRC20",
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(payment.CryptoAmount, payment.Token, "to", payment.DepositAddress)
}
```

Then [set your webhook URL](#webhooksset) and fulfil the order when
[`payment.confirmed`](#a-webhook-ipn-handler-with-nethttp) arrives.

## Configuration

### NewClient

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

| Parameter | Type | Required | Description |
|---|---|---|---|
| `apiKey` | string | yes | Your secret key, test or live ([Authentication](/docs/authentication)) |
| `opts` | `...Option` | no | The options below |

| Option | Default | Description |
|---|---|---|
| `WithBaseURL(u string)` | `PAYITGO_BASE_URL`, then the address the package was released with | The gateway's address, `https://api.payitgo.co`. Trailing slashes are removed |
| `WithHTTPClient(h *http.Client)` | `&http.Client{Timeout: 30 * time.Second}` | The HTTP client every request goes through: your timeout, transport, proxy or tracing. Its `Timeout` is the limit for one attempt; a zero `Timeout` means none |
| `WithMaxRetries(n int)` | `2` | How many times a request is retried after a network error, a `429` or a `5xx`. `0` turns retries off |
| `WithWebhookSecret(secret string)` | none | Your mode's signing secret. [`Webhooks.ConstructEvent`](#webhooksconstructevent) uses it when you pass none |
| `WithWebhookURL(u string)` | none | Sent as `webhook_url` with every [`Payments.Create`](#paymentscreate) whose `PaymentParams.WebhookURL` is empty |

`NewClient` makes no request. It returns an error when:

| Error | Cause |
|---|---|
| `payitgo: the API key is required` | `apiKey` is empty |
| `payitgo: set WithBaseURL (or PAYITGO_BASE_URL) to your gateway's address` | No address from the option, the environment or the package (`WithBaseURL("")` clears the others) |

```go
client, err := payitgo.NewClient(os.Getenv("PAYITGO_KEY"),
	payitgo.WithBaseURL("https://api.payitgo.co"), // or set PAYITGO_BASE_URL
	payitgo.WithHTTPClient(&http.Client{Timeout: 15 * time.Second}),
	payitgo.WithMaxRetries(2),
	payitgo.WithWebhookSecret(os.Getenv("PAYITGO_WEBHOOK_SECRET")), // the signing secret of the key's mode
	payitgo.WithWebhookURL("https://acme.example/payitgo/ipn"),     // optional default for Payments.Create
)
if err != nil {
	log.Fatal(err)
}
```

A `*Client` is safe for concurrent use: create one and share it.

### The Client

```go
type Client struct {
	Payments *PaymentsService
	Tokens   *TokensService
	Checkout *CheckoutService
	Webhooks *WebhooksService
	// unexported fields
}
```

`payitgo.Version` (`"0.1.0"`) is sent in the `User-Agent: payitgo-go/0.1.0`
header.

### IsLive

`GET /v1/webhook` · [API reference](/docs/webhooks#read-the-webhook)

```go
func (c *Client) IsLive(ctx context.Context) (bool, error)
```

**Returns** `true` for a live key, `false` for a test key. A key does not say
its mode, so the first call asks payitgo (the `Livemode` of the key's
[webhook](#webhooksget); a read-only key may call it) and the answer is kept
for the client's life: later calls make no request. Safe for concurrent use.

**Errors** a [`*payitgo.Error`](#payitgoerror), as any call: `401` for a key
payitgo does not know, `403` for a live key while live mode is off. An error
is not kept: the next call asks again.

```go
live, err := client.IsLive(ctx)
if err != nil {
	return err
}
if !live {
	log.Println("test mode: payments are simulated")
}
```

### Base URL

The gateway address is taken from, in order: `WithBaseURL`, the
`PAYITGO_BASE_URL` environment variable, and the address the package was
released with:

```bash
export PAYITGO_BASE_URL=https://api.payitgo.co
```

### Timeouts, cancellation and the HTTP client

Each request is sent with the client's `*http.Client` (30-second timeout by
default; replace it with `WithHTTPClient`) and your `ctx`. The context bounds
the whole call, retries and waits included; a cancelled or expired context
stops it at once and is not retried. Requests carry the headers
`Authorization: Bearer <key>`, `Accept: application/json`,
`Content-Type: application/json` (when there is a body) and
`User-Agent: payitgo-go/0.1.0`.

```go
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
payment, err := client.Payments.Get(ctx, "ORD-1001")
if err != nil {
	return err // a *payitgo.Error, or ctx.Err() once the 10 seconds are up
}
```

### Test and live mode

The key decides the mode. A test key creates and reads test payments
(no coins: you [simulate](#paymentssimulate) what the customer sends) and
manages the test webhook; a live key does the same for live ones. The key
does not say which it is: the dashboard shows a Test or Live badge next to
it, and [`client.IsLive(ctx)`](#islive) asks payitgo. Pair each key with the
webhook secret of the same mode. See [Test mode](#test-mode).

## Errors

### payitgo.Error

A request the API refused, or that got no answer, returns a `*payitgo.Error`:

```go
type Error struct {
	Status  int    // the HTTP status; 0 when no answer came
	Message string // the API's error message
}

func (e *Error) Error() string // "payitgo: <Message> (HTTP <Status>)", or "payitgo: <Message>" when Status is 0
```

| Field | Description |
|---|---|
| `Status` | The HTTP status, or `0` when no answer came (network error or timeout, after the retries) |
| `Message` | The API's `error` message, such as `minimum payment in USDT_TRC20 is $6.44 right now (network fees)`. The status text (such as `Bad Gateway`) when the answer had no message. `request failed: <cause>` or `reading the response: <cause>` when `Status` is 0 |

Read it with `errors.As`, branch on `Status`, and log `Message` (its wording
may change, see [Errors and limits](/docs/errors)):

| `Status` | Meaning | Retried by the library |
|---|---|---|
| `0` | No answer: DNS, connection, TLS or timeout | Yes |
| `400` | The request is invalid (the message says which field) | No |
| `401` | `missing api key` or `invalid api key` | No |
| `403` | Not allowed, for example a live key while live mode is off, or a storefront key changing the account's webhook | No |
| `404` | No such payment in the key's mode | No |
| `409` | Conflicts with the current state, such as choosing a payment's coin twice, or simulating a payment whose simulation is running | No |
| `429` | Over the [rate limit](/docs/errors#rate-limits) | Yes |
| `500` and other `5xx` | Something failed on payitgo's side | Yes |

```go
_, err := client.Payments.Create(ctx, &payitgo.PaymentParams{OrderID: "ORD-1", AmountUSD: "1", Token: "BTC"})
var perr *payitgo.Error
if errors.As(err, &perr) && perr.Status == http.StatusBadRequest {
	log.Printf("payitgo refused: %s", perr.Message) // e.g. an amount under the coin's minimum
} else if err != nil {
	return err
}
```

Two other kinds of error can come back: `ctx.Err()` (`context.Canceled` or
`context.DeadlineExceeded`) when the context ends while the library waits to
retry, and an `encoding/json` error when a successful answer is not the JSON
the method expects.

### payitgo.ErrSignature

[`ConstructEvent`](#webhooksconstructevent) returns an error wrapping
`payitgo.ErrSignature` when a notification cannot be trusted. Check it with
`errors.Is` and answer `400`. Its text is
`payitgo: webhook signature verification failed: <reason>`:

| Reason | Cause |
|---|---|
| `missing or malformed X-Webhook-Signature header` | No header, or no `t` or `v1` in it |
| `signature does not match` | The body, the timestamp or the secret differ from what payitgo signed |
| `malformed timestamp` | `t` is not a whole number of seconds (checked unless the tolerance is `0`) |
| `timestamp outside the tolerance` | `t` is further than the tolerance (5 minutes) from your clock |
| `no webhook secret: pass one, or use WithWebhookSecret` | The client method got no secret and the client has no `WithWebhookSecret` |

```go
event, err := client.Webhooks.ConstructEvent(body, r.Header.Get("X-Webhook-Signature"))
if errors.Is(err, payitgo.ErrSignature) {
	http.Error(w, "bad signature", http.StatusBadRequest)
	return
}
```

A body that is correctly signed but is not JSON returns an `encoding/json`
error instead; payitgo never sends one. Answer `400` to any error from
`ConstructEvent`.

### Retries

A request is retried, up to `WithMaxRetries` times (default 2, so at most 3
attempts), after a network error or timeout, a `429` or a `5xx`. It waits
0.5 s before the first retry and 1 s before the second (500 ms × 2ⁿ), or less
if `ctx` ends first. Other statuses are never retried. When the last attempt
fails, the call returns a `*payitgo.Error` with that attempt's status (`0`,
`429` or `5xx`).

Every call is retried the same way, and each is safe to repeat:

| Call | Repeating it |
|---|---|
| `Payments.Create` | Returns the order's open payment instead of a second one ([safe retries](/docs/payments#safe-retries)) |
| `Payments.SelectToken`, `Checkout.SelectToken` | If the first attempt went through, the repeat returns `409`: read the payment with `Get`, it has its coin |
| `Payments.Simulate` | If the first attempt went through, the repeat returns `409` while that simulation runs: read the payment with `Get` |
| `Webhooks.Set`, `Webhooks.Remove` | Changes nothing the second time |
| `Webhooks.Test` | Queues one more `ping` |
| Reads | Change nothing |

## Payments

### Payments.Create

`POST /v1/payments` · [API reference](/docs/payments#create-a-payment)

```go
func (s *PaymentsService) Create(ctx context.Context, p *PaymentParams) (*Payment, error)
```

Creates a payment for one of your orders. With a `Token`, the payment comes
back with its deposit address, amount, QR payload and a 15-minute deadline.
Without one, it comes back with `TokenOptions` and the customer (or your page,
with [`SelectToken`](#paymentsselecttoken)) chooses the coin. While the order
has an open payment, creating it again returns that payment.

`p` must not be nil. Its fields ([`PaymentParams`](#paymentparams)):

| Field | JSON | Type | Required | Description |
|---|---|---|---|---|
| `OrderID` | `order_id` | string | yes | Your id for the order, 1 to 128 bytes. One open payment per order and mode |
| `AmountUSD` | `amount_usd` | string | yes, unless `PriceAmount` + `PriceCurrency` | The price in US dollars, as a decimal string: `"49.90"` |
| `PriceAmount` | `price_amount` | string | with `PriceCurrency` | Instead of `AmountUSD`: the price in another currency, as a decimal string: `"45.50"`. payitgo converts it into dollars at the rate of the moment ([prices in other currencies](/docs/payments#prices-in-other-currencies)) |
| `PriceCurrency` | `price_currency` | string | with `PriceAmount` | The ISO 4217 code of its currency, such as `"EUR"`, `"GBP"` or `"NGN"` (any case) |
| `Token` | `token` | string | no | The coin, such as `"USDT_TRC20"` (from [`Tokens.List`](#tokenslist)). Empty: the customer chooses |
| `Tokens` | `tokens` | `[]string` | no | Without `Token`: the coins to offer, such as `[]string{"USDT_TRC20", "USDT_BEP20"}`. Default: every coin you accept whose minimum the amount reaches |
| `Description` | `description` | string | no | What the customer pays for, up to 500 bytes |
| `Metadata` | `metadata` | `map[string]string` | no | Your own data: up to 20 keys of up to 40 bytes, values of up to 500 |
| `CustomerEmail` | `customer_email` | string | no | The customer's email address |
| `SuccessURL` | `success_url` | string | no | `http(s)` URL the hosted checkout links to once the payment is confirmed |
| `CancelURL` | `cancel_url` | string | no | `http(s)` URL the hosted checkout links back to if the payment closes unpaid |
| `WebhookURL` | `webhook_url` | string | no | `https://` URL that gets this payment's notifications instead of your account's webhook URL ([per-payment URL](/docs/webhooks#per-payment-webhook-url)). Empty: the client's `WithWebhookURL`, if any |
| `Simulate` | `simulate` | string | no | Test keys only: the outcome to [simulate](#paymentssimulate), a `payitgo.Outcome…` constant. With `Token` it starts at once; without, once the coin is chosen |

Empty optional fields are left out of the request.

**Returns** the [`*Payment`](#payment): the new one (`201`) or the order's
open one (`200`); both look the same. Fields: [the payment
object](/docs/payments#the-payment-object).

**Errors** `*payitgo.Error` with `Status` `400` for an invalid request: a missing
`order_id`, `amount_usd must be > 0`, a token you do not accept, an amount
under the token's minimum, a `webhook_url` that is not public `https`, a
`simulate` with a live key or an unknown outcome, a price in another
currency given with `amount_usd`, without its pair or in a code that is no
currency, and the rest of the [create errors](/docs/payments#errors). `503`
when no exchange rate can be had for `price_currency` (after the
[retries](#retries)): try again shortly. Also `401`, `403`, `429`,
`500` and `0` as for [every call](#payitgoerror).

```go
// The customer pays in a coin you know:
payment, err := client.Payments.Create(ctx, &payitgo.PaymentParams{
	OrderID:       "ORD-1001",
	AmountUSD:     "49.90",
	Token:         "USDT_TRC20",
	Description:   "2 x Acme T-shirt",
	Metadata:      map[string]string{"customer_id": "cus_42"},
	CustomerEmail: "jane@example.com",
	SuccessURL:    "https://acme.example/thanks",
	CancelURL:     "https://acme.example/cart",
})
if err != nil {
	return err
}
// payment.DepositAddress, payment.CryptoAmount, payment.QRPayload, payment.ExpiresAt

// The customer chooses the coin (on the hosted page, or yours):
open, err := client.Payments.Create(ctx, &payitgo.PaymentParams{OrderID: "ORD-1002", AmountUSD: "25"})
if err != nil {
	return err
}
for _, t := range open.TokenOptions {
	fmt.Println(t.Token, t.MinUSD, t.AmountDueUSD)
}

// This payment's notifications go to its own URL:
_, err = client.Payments.Create(ctx, &payitgo.PaymentParams{
	OrderID: "ORD-1003", AmountUSD: "30", Token: "USDT_TRC20",
	WebhookURL: "https://acme.example/orders/ORD-1003/ipn",
})

// Priced in euros: converted into dollars at the rate of the moment.
priced, err := client.Payments.Create(ctx, &payitgo.PaymentParams{
	OrderID: "ORD-1004", PriceAmount: "45.50", PriceCurrency: "EUR",
})
// priced.PriceAmount "45.5", priced.PriceCurrency "EUR", priced.PriceRate "1.082", priced.USDAmount "49.23"
```

### Payments.Get

`GET /v1/payments/{id}` · [API reference](/docs/payments#retrieve-a-payment)

```go
func (s *PaymentsService) Get(ctx context.Context, id string) (*Payment, error)
```

| Parameter | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | The payment's id, or your `order_id` for the order's **open** payment (the payment id is tried first). Escaped for the URL by the library |

**Returns** the [`*Payment`](#payment).

**Errors** `*payitgo.Error` `404` (`payment not found`) for an unknown id, a
payment of the other mode, or an order with no open payment. Plus the
[common statuses](#payitgoerror).

```go
byID, err := client.Payments.Get(ctx, "8f3a2c1e9b7d4a6f05e1c2d3")
if err != nil {
	return err
}
byOrder, err := client.Payments.Get(ctx, "ORD-1001") // the order's open payment
if err != nil {
	return err
}
if byID.Status == payitgo.StatusConfirmed {
	fmt.Println("paid at", byID.ConfirmedAt, "order", byOrder.OrderID)
}
```

### Payments.List

`GET /v1/payments` · [API reference](/docs/payments#list-payments)

```go
func (s *PaymentsService) List(ctx context.Context, p *ListParams) (*PaymentList, error)
```

Payments of the key's mode, newest first. `p` may be nil. Its fields
([`ListParams`](#listparams)), each left out of the query when it is the zero
value:

| Field | Type | Default | Description |
|---|---|---|---|
| `Status` | string | all | Only payments in this [status](/docs/payments#lifecycle): one of the `payitgo.Status…` constants |
| `Chain` | string | all | Only payments on this network, such as `"tron"` |
| `Token` | string | all | Only payments in this coin, such as `"USDT_TRC20"` |
| `StoreID` | string | all | Only payments of this [store](/docs/account#stores) (ignored for a storefront key, which sees its own) |
| `Limit` | int | `50` | Page size, 1 to 200 (above 200 the API uses 50) |
| `Offset` | int | `0` | How many to skip |

**Returns** a [`*PaymentList`](#paymentlist): `Payments`, `Total` (matches
across all pages), and the `Limit` and `Offset` applied.

**Errors** the [common statuses](#payitgoerror) only. An unknown status, chain or
token matches nothing.

```go
// Every confirmed payment, page by page:
var confirmed []payitgo.Payment
for offset := 0; ; {
	page, err := client.Payments.List(ctx, &payitgo.ListParams{Status: payitgo.StatusConfirmed, Limit: 100, Offset: offset})
	if err != nil {
		return err
	}
	confirmed = append(confirmed, page.Payments...)
	offset += page.Limit
	if offset >= page.Total {
		break
	}
}
```

### Payments.SelectToken

`POST /v1/payments/{id}/token` · [API reference](/docs/payments#choose-the-coin-later)

```go
func (s *PaymentsService) SelectToken(ctx context.Context, id, token string) (*Payment, error)
```

Sets the coin of a payment created without one, once the customer picked it on
[your own page](#your-own-payment-page). The coin can be set once.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | The payment's id or your `order_id` |
| `token` | string | yes | One of the payment's `TokenOptions[].Token`, such as `"USDT_TRC20"` |

**Returns** the whole [`*Payment`](#payment), now with `DepositAddress`,
`CryptoAmount`, `RateLocked`, `QRPayload` and an `ExpiresAt` 15 minutes away.

**Errors** `*payitgo.Error`:

| `Status` | `Message` | Cause |
|---|---|---|
| `400` | `token is required` | Empty `token` |
| `400` | `token is not one of this payment's options` | A coin not in `TokenOptions` |
| `400` | `minimum payment in … is $… right now (it is converted on its way to the merchant, and the exchange takes no less)` | The coin needs a bigger payment than this one: it is converted through the exchange on its way to the merchant, which takes no less, and the merchant pays the fees or hides such coins (`hide_below_minimum`). Offer the other coins |
| `400` | `this payment can no longer be paid` | The payment is no longer `pending`, or its hour to choose has passed |
| `404` | `payment not found` | No such payment in the key's mode |
| `409` | `the coin for this payment has already been chosen` | The coin was set before (by you or on the hosted page) |

```go
payment, err := client.Payments.Create(ctx, &payitgo.PaymentParams{OrderID: "ORD-1002", AmountUSD: "25"})
if err != nil {
	return err
}
// …your page lists payment.TokenOptions, the customer picks one…
chosen, err := client.Payments.SelectToken(ctx, payment.ID, "USDT_BEP20")
if err != nil {
	return err
}
fmt.Println(chosen.CryptoAmount, chosen.Token, chosen.DepositAddress, chosen.ExpiresAt)
```

### Payments.Simulate

`POST /v1/payments/{id}/simulate` · [API reference](/docs/testing#simulate-a-payment)

```go
func (s *PaymentsService) Simulate(ctx context.Context, id, outcome string) (*Payment, error)
```

Plays an outcome on a test payment: payitgo sends its deposits, status changes
and webhooks about 5 seconds apart, as live coins would, with no coins
([Test mode](/docs/testing)). Test keys only.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | The payment's id or your `order_id` |
| `outcome` | string | yes | A `payitgo.Outcome…` constant ([what each does](/docs/testing#outcomes)) |

**Returns** the whole [`*Payment`](#payment), with its `Simulation` started.
Simulated deposits have `Simulated` true; their `TxID` names no transaction on any chain.

**Errors** `*payitgo.Error`:

| `Status` | `Message` | Cause |
|---|---|---|
| `400` | `outcome is required` | Empty `outcome` |
| `400` | `simulate must be one of: confirmed, overpaid, …` | An unknown outcome |
| `400` | `only test payments can be simulated: use a test key` | A live key |
| `400` | `choose the payment's coin first` | No coin yet, for anything but `expired` |
| `400` | `this payment is confirmed: only a pending or partially paid payment can be simulated` | The payment is closed or confirming |
| `400` | `a BTC address only receives BTC: wrong_asset needs a coin on Tron, Ethereum or BNB Smart Chain` | `wrong_asset` on a coin that has no other |
| `404` | `payment not found` | No such payment in the key's mode |
| `409` | `a simulation is already running for this payment` | Wait until its `State` is `done` |

```go
payment, err := client.Payments.Simulate(ctx, "ORD-1002", payitgo.OutcomePartiallyPaid)
if err != nil {
	return err
}
fmt.Println(payment.Simulation.Outcome, payment.Simulation.State) // partially_paid running
// once it is done, pay the rest:
payment, err = client.Payments.Simulate(ctx, "ORD-1002", payitgo.OutcomeConfirmed)
```

## Tokens

### Tokens.List

`GET /v1/tokens` · [API reference](/docs/payments#list-tokens)

```go
func (s *TokensService) List(ctx context.Context, p *TokenListParams) ([]TokenInfo, error)
```

The coins your customers can pay with in the key's mode, with today's network
fee and minimum. With an amount, only the coins a payment of that amount
offers (its `token_options`), each with `AmountDueUSD` when you pass the fees
on.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `p` | `*TokenListParams` | no | `nil` for every coin, or `&payitgo.TokenListParams{AmountUSD: "49.90"}` for the coins of a payment of that amount |

**Returns** a `[]`[`TokenInfo`](#tokeninfo) (the library unwraps the API's
`{"tokens": […]}`).

**Errors** `*payitgo.Error` `400` (`amount_usd must be > 0`) for an amount that
is not a positive number, and the [common statuses](#payitgoerror).

```go
tokens, err := client.Tokens.List(ctx, &payitgo.TokenListParams{AmountUSD: "49.90"})
if err != nil {
	return err
}
for _, t := range tokens {
	fmt.Printf("%s on %s: pay $%s, %s\n", t.Token, t.Chain, t.AmountDueUSD, t.FeeNote)
}
```

## Webhooks

The webhook (IPN) of the key's mode: one URL that receives every notification
of that mode, and the secret that signs them
([Webhooks](/docs/webhooks#the-webhook-api)). A test key manages the test
webhook, a live key the live one. A [storefront
key](/docs/authentication#storefront-keys) can `Get` and `Test` it, but not
`Set` or `Remove` it.

### Webhooks.Get

`GET /v1/webhook` · [API reference](/docs/webhooks#read-the-webhook)

```go
func (s *WebhooksService) Get(ctx context.Context) (*Webhook, error)
```

**Returns** the [`*Webhook`](#webhook): `URL` (`""` when none is set) and
`Secret`, the signing secret to pass to `WithWebhookSecret`. It
always succeeds for a valid key.

```go
webhook, err := client.Webhooks.Get(ctx)
if err != nil {
	return err
}
fmt.Println(webhook.URL, webhook.Secret)
```

### Webhooks.Set

`PUT /v1/webhook` · [API reference](/docs/webhooks#set-the-url)

```go
func (s *WebhooksService) Set(ctx context.Context, url string) (*Webhook, error)
```

| Parameter | Type | Required | Description |
|---|---|---|---|
| `url` | string | yes | An absolute `https://` URL on a public host that accepts `POST` |

**Returns** the [`*Webhook`](#webhook) with its new `URL`. The secret does not
change. Setting the URL it already has changes nothing.

**Errors** `*payitgo.Error` `400` (`url is required`, `url must use https`,
`url must be an absolute http(s) URL`, `url host does not resolve`,
`url must point at a public host`), `403` for a storefront key, `500`
(`could not set the webhook`), and the [common statuses](#payitgoerror).

```go
if _, err := client.Webhooks.Set(ctx, "https://acme.example/payitgo/ipn"); err != nil {
	return err
}
```

### Webhooks.Remove

`DELETE /v1/webhook` · [API reference](/docs/webhooks#remove-the-url)

```go
func (s *WebhooksService) Remove(ctx context.Context) (*Webhook, error)
```

Stops posting the mode's notifications to its URL. Payments created with their
own `webhook_url` still get theirs, and the secret is kept.

**Returns** the [`*Webhook`](#webhook) with `URL` `""`.

**Errors** `403` for a storefront key, `500` (`could not remove the webhook`),
and the [common statuses](#payitgoerror).

### Webhooks.Test

`POST /v1/webhook/test` · [API reference](/docs/webhooks#send-a-test)

```go
func (s *WebhooksService) Test(ctx context.Context) (*WebhookDelivery, error)
```

Queues a signed `ping` notification to the mode's URL; it arrives within about
5 seconds.

**Returns** the [`*WebhookDelivery`](#webhookdelivery), with `Status`
`payitgo.DeliveryPending`.

**Errors** `*payitgo.Error` `400` (`no webhook url is set for this mode: set one
first`), `500` (`could not queue the test`), and the
[common statuses](#payitgoerror).

```go
delivery, err := client.Webhooks.Test(ctx)
if err != nil {
	return err
}
fmt.Println(delivery.ID, delivery.EventType, delivery.URL) // 6710c3a1f2b4e5d6a7b8c9d0 ping https://…
```

### Webhooks.ConstructEvent

No request · [how verification works](/docs/webhooks#verifying-the-signature)

```go
func (s *WebhooksService) ConstructEvent(payload []byte, signatureHeader string, secret ...string) (*Event, error)
```

Verifies a notification with a 5-minute tolerance and returns its event. It
computes the HMAC-SHA256 of `t + "." + payload` with the secret, compares it
with `v1` in constant time (`hmac.Equal`), checks that `t` is within 5 minutes
of your clock, and only then parses the body.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `payload` | `[]byte` | yes | The **raw** request body, exactly as received (`io.ReadAll(r.Body)`). A body decoded and encoded again will not verify ([keep the raw body](/docs/webhooks#keep-the-raw-body)) |
| `signatureHeader` | string | yes | `r.Header.Get("X-Webhook-Signature")` |
| `secret` | string | no | The signing secret, as payitgo gave it. Default: the client's `WithWebhookSecret`. A non-empty secret passed here wins |

For another tolerance, use
[`payitgo.ConstructEventWithTolerance`](#constructevent-and-constructeventwithtolerance).

**Returns** the [`*Event`](#event).

**Errors** an error wrapping [`payitgo.ErrSignature`](#payitgoerrsignature) with
the reason, or an `encoding/json` error for a signed body that is not JSON.
Answer `400`.

```go
event, err := client.Webhooks.ConstructEvent(body, r.Header.Get("X-Webhook-Signature"))
if err != nil {
	http.Error(w, "bad signature", http.StatusBadRequest)
	return
}
if event.Type == payitgo.EventPaymentConfirmed {
	p, err := event.Payment()
	if err == nil {
		fmt.Println(p.OrderID)
	}
}
```

### ConstructEvent and ConstructEventWithTolerance

```go
const DefaultTolerance = 5 * time.Minute

func ConstructEvent(payload []byte, signatureHeader, secret string) (*Event, error)
func ConstructEventWithTolerance(payload []byte, signatureHeader, secret string, tolerance time.Duration) (*Event, error)
```

The same check without a client, for code that has the secret but no API key.
`ConstructEvent` uses `DefaultTolerance`. `ConstructEventWithTolerance` takes
your own; `0` turns the timestamp check off (only for tests with captured
bodies).

```go
event, err := payitgo.ConstructEvent(body, r.Header.Get("X-Webhook-Signature"), os.Getenv("PAYITGO_WEBHOOK_SECRET"))
if err != nil {
	http.Error(w, "bad signature", http.StatusBadRequest)
	return
}
```

**Errors** an error wrapping [`payitgo.ErrSignature`](#payitgoerrsignature) with
the reason, or an `encoding/json` error for a signed body that is not JSON.

## Checkout

These two calls use the [public checkout API](/docs/checkout#the-public-checkout-api):
they are sent **without** your API key and are keyed by the payment's
`checkout_token`. They return the payer's view of a payment. On your
server you rarely need them: `Payments.Get` and `Payments.SelectToken` return
more. The client still needs a valid key to be created.

### Checkout.Get

`GET /checkout/{checkout_token}` · [API reference](/docs/checkout#read-the-payment)

```go
func (s *CheckoutService) Get(ctx context.Context, checkoutToken string) (*Payment, error)
```

| Parameter | Type | Required | Description |
|---|---|---|---|
| `checkoutToken` | string | yes | The payment's `CheckoutToken` |

**Returns** a [`*Payment`](#payment) without your private fields:
`MerchantID`, `Metadata`, `CustomerEmail`, `WebhookURL`, `CheckoutToken` and
`CheckoutURL` are empty.

**Errors** `*payitgo.Error` `404` (`payment not found`), `0`, `429` and `5xx`.

### Checkout.SelectToken

`POST /checkout/{checkout_token}/token` · [API reference](/docs/checkout#choosing-the-coin)

```go
func (s *CheckoutService) SelectToken(ctx context.Context, checkoutToken, token string) (*Payment, error)
```

| Parameter | Type | Required | Description |
|---|---|---|---|
| `checkoutToken` | string | yes | The payment's `CheckoutToken` |
| `token` | string | yes | One of the payment's `TokenOptions[].Token` |

**Returns** the payer's view of the [`*Payment`](#payment) with its coin,
address, amount and a 15-minute deadline.

**Errors** the same as [`Payments.SelectToken`](#paymentsselecttoken), and
`429` (`rate limit exceeded`) beyond 5 choices a second from one IP address.

```go
view, err := client.Checkout.Get(ctx, "b28d4f61e9a03c75d2e8f1a6")
if err != nil {
	return err
}
if view.Token == "" {
	chosen, err := client.Checkout.SelectToken(ctx, "b28d4f61e9a03c75d2e8f1a6", "USDT_TRC20")
	if err != nil {
		return err
	}
	fmt.Println(chosen.DepositAddress)
}
```

## Types

Amounts are decimal strings (`"49.9"`): parse them with a decimal package, not
`float64`. Times are `time.Time`, optional times `*time.Time`. Optional fields
the API leaves out are the zero value (`""`, `false`, `nil`).

### Payment

[The payment object](/docs/payments#the-payment-object).

```go
type Payment struct {
	ID                 string            `json:"id"`
	MerchantID         string            `json:"merchant_id,omitempty"`
	StoreID            string            `json:"store_id,omitempty"`
	OrderID            string            `json:"order_id"`
	Status             string            `json:"status"`     // a Status… constant
	Livemode           bool              `json:"livemode"`
	Simulation         *Simulation       `json:"simulation,omitempty"` // a test payment's simulated outcome
	Token              string            `json:"token"`      // "" until the coin is chosen
	Chain              string            `json:"chain"`      // "" until the coin is chosen
	TokenOptions       []TokenInfo       `json:"token_options,omitempty"` // only while Token is ""
	DepositAddress     string            `json:"deposit_address"`
	DestinationTag     *uint32           `json:"destination_tag,omitempty"` // XRP only
	CryptoAmount       string            `json:"crypto_amount"`
	ReceivedAmount     string            `json:"received_amount"`
	USDAmount          string            `json:"usd_amount"`
	PriceAmount        string            `json:"price_amount,omitempty"`   // your price in another currency, when you gave one
	PriceCurrency      string            `json:"price_currency,omitempty"` // its ISO 4217 code, upper case: "EUR"
	PriceRate          string            `json:"price_rate,omitempty"`     // dollars per unit of PriceCurrency
	AmountDueUSD       string            `json:"amount_due_usd"`
	FeesPaidByCustomer bool              `json:"fees_paid_by_customer"`
	NetworkFee         string            `json:"network_fee"`
	NetworkFeeUSD      string            `json:"network_fee_usd"`
	Settlement         string            `json:"settlement"` // "custodial" or "direct"
	ReceivedUSD        string            `json:"received_usd"`
	RateLocked         string            `json:"rate_locked"`
	TxID               string            `json:"tx_id,omitempty"`
	Overpaid           bool              `json:"overpaid,omitempty"`
	PaidLate           bool              `json:"paid_late,omitempty"`
	Deposits           []Deposit         `json:"deposits"`
	ExpiresAt          time.Time         `json:"expires_at"`
	CreatedAt          time.Time         `json:"created_at"`
	ConfirmedAt        *time.Time        `json:"confirmed_at,omitempty"`
	NetworkFeeNote     string            `json:"network_fee_note"`
	QRPayload          string            `json:"qr_payload"`
	CheckoutToken      string            `json:"checkout_token,omitempty"`
	CheckoutURL        string            `json:"checkout_url,omitempty"`
	Description        string            `json:"description,omitempty"`
	Metadata           map[string]string `json:"metadata,omitempty"`
	CustomerEmail      string            `json:"customer_email,omitempty"`
	SuccessURL         string            `json:"success_url,omitempty"`
	CancelURL          string            `json:"cancel_url,omitempty"`
	WebhookURL         string            `json:"webhook_url,omitempty"`
}
```

Statuses ([lifecycle](/docs/payments#lifecycle)):

```go
const (
	StatusPending       = "pending"
	StatusPartiallyPaid = "partially_paid"
	StatusDetecting     = "detecting"
	StatusConfirmed     = "confirmed" // the only one that means you have been paid
	StatusUnderpaid     = "underpaid"
	StatusExpired       = "expired"
	StatusFailed        = "failed"
)
```

### Simulation

A test payment's [simulated outcome](/docs/testing#simulate-a-payment). `nil`
when it has none.

```go
type Simulation struct {
	Outcome string `json:"outcome"` // an Outcome… constant
	State   string `json:"state"`   // a Simulation… constant
}

const (
	OutcomeConfirmed     = "confirmed"
	OutcomeOverpaid      = "overpaid"
	OutcomePartiallyPaid = "partially_paid"
	OutcomeUnderpaid     = "underpaid"
	OutcomeExpired       = "expired"
	OutcomeFailed        = "failed"
	OutcomeWrongAsset    = "wrong_asset"
	OutcomePaidLate      = "paid_late"

	SimulationRequested = "requested" // asked for on create, waiting for the coin
	SimulationRunning   = "running"
	SimulationDone      = "done"
)
```

### Deposit

One transfer to the payment's address ([the deposit object](/docs/payments#the-deposit-object)).

```go
type Deposit struct {
	TxID        string     `json:"tx_id"`
	Token       string     `json:"token"` // the asset that arrived
	Chain       string     `json:"chain"`
	Amount      string     `json:"amount"`
	USDValue    string     `json:"usd_value"`
	Counted     bool       `json:"counted"`        // counts toward the payment
	Late        bool       `json:"late,omitempty"` // arrived after the payment had closed
	Status      string     `json:"status"`         // "pending", "confirmed" or "failed"
	SeenAt      time.Time  `json:"seen_at"`
	ConfirmedAt *time.Time `json:"confirmed_at,omitempty"`
	From        string     `json:"from,omitempty"`        // the sender, when the network reports it: where a refund would go
	Block       int64      `json:"block,omitempty"`       // the block (XRP: the ledger) that holds it
	MadeAt      *time.Time `json:"made_at,omitempty"`     // that block's time
	FailedAt    *time.Time `json:"failed_at,omitempty"`   // when it failed
	FailReason  string     `json:"fail_reason,omitempty"` // "transaction not found on chain" or "confirmation timeout"
	Simulated   bool       `json:"simulated,omitempty"`   // played by a test payment's simulation: its TxID is on no chain
}
```

### PaymentParams

The parameters of [`Payments.Create`](#paymentscreate).

```go
type PaymentParams struct {
	OrderID       string            `json:"order_id"`
	AmountUSD     string            `json:"amount_usd,omitempty"`     // the price in dollars, or:
	PriceAmount   string            `json:"price_amount,omitempty"`   // the price in another currency…
	PriceCurrency string            `json:"price_currency,omitempty"` // …and its ISO 4217 code ("EUR")
	Token         string            `json:"token,omitempty"`
	Tokens        []string          `json:"tokens,omitempty"`
	Description   string            `json:"description,omitempty"`
	Metadata      map[string]string `json:"metadata,omitempty"`
	CustomerEmail string            `json:"customer_email,omitempty"`
	SuccessURL    string            `json:"success_url,omitempty"`
	CancelURL     string            `json:"cancel_url,omitempty"`
	WebhookURL    string            `json:"webhook_url,omitempty"`
	Simulate      string            `json:"simulate,omitempty"` // test keys only: an Outcome… constant
}
```

### ListParams

The parameters of [`Payments.List`](#paymentslist).

```go
type ListParams struct {
	Status  string
	Chain   string
	Token   string
	StoreID string // store_id
	Limit   int
	Offset  int
}
```

### PaymentList

```go
type PaymentList struct {
	Payments []Payment `json:"payments"`
	Total    int       `json:"total"`  // matches across all pages
	Limit    int       `json:"limit"`  // the page size applied
	Offset   int       `json:"offset"` // the offset applied
}
```

### TokenInfo

A coin the customer can pay with ([the TokenInfo object](/docs/payments#the-tokeninfo-object)).

```go
type TokenInfo struct {
	Token         string `json:"token"`
	Chain         string `json:"chain"`
	MinUSD        string `json:"min_usd"`
	NetworkFeeUSD string `json:"network_fee_usd"`
	AmountDueUSD  string `json:"amount_due_usd,omitempty"` // with an amount, when you pass the fees on
	FeeNote       string `json:"fee_note"`
}

type TokenListParams struct {
	AmountUSD string // "" lists every coin
}
```

### Webhook

The webhook of the key's mode ([the webhook object](/docs/webhooks#the-webhook-object)).

```go
type Webhook struct {
	Livemode  bool      `json:"livemode"`
	URL       string    `json:"url"`    // "" when none is set (null in the API)
	Secret    string    `json:"secret"` // signs the mode's notifications
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}
```

### WebhookDelivery

One notification on its way to one URL ([the delivery object](/docs/webhooks#the-delivery-object)).

```go
type WebhookDelivery struct {
	ID             string                   `json:"id"`
	MerchantID     string                   `json:"merchant_id"`
	Target         string                   `json:"target"` // TargetAccount or TargetPayment
	URL            string                   `json:"url"`
	EventID        string                   `json:"event_id"`
	EventType      string                   `json:"event_type"`
	Payload        string                   `json:"payload"` // the exact body posted
	Status         string                   `json:"status"`  // a Delivery… constant
	Attempts       int                      `json:"attempts"`
	MaxAttempts    int                      `json:"max_attempts"`
	NextAttemptAt  time.Time                `json:"next_attempt_at"`
	LastStatusCode int                      `json:"last_status_code,omitempty"`
	LastError      string                   `json:"last_error,omitempty"`
	LastAttemptAt  *time.Time               `json:"last_attempt_at,omitempty"`
	DeliveredAt    *time.Time               `json:"delivered_at,omitempty"`
	ResolvedAt     *time.Time               `json:"resolved_at,omitempty"`
	ResolutionNote string                   `json:"resolution_note,omitempty"`
	CreatedAt      time.Time                `json:"created_at"`
	History        []WebhookDeliveryAttempt `json:"history,omitempty"`
}

type WebhookDeliveryAttempt struct {
	At         time.Time `json:"at"`
	URL        string    `json:"url,omitempty"`
	Signed     string    `json:"signed,omitempty"` // the t of the attempt's signature
	StatusCode int       `json:"status_code,omitempty"`
	Error      string    `json:"error,omitempty"`
	Response   string    `json:"response,omitempty"` // the start of your server's answer
	Millis     int64     `json:"millis,omitempty"`
	Note       string    `json:"note,omitempty"` // a redelivery or resolution, and by whom
}

const (
	DeliveryPending   = "pending"
	DeliverySending   = "sending"
	DeliveryDelivered = "delivered"
	DeliveryFailed    = "failed"
	DeliveryResolved  = "resolved"

	TargetAccount = "account" // the mode's webhook URL
	TargetPayment = "payment" // the payment's own webhook_url
)
```

### Event

A verified notification ([the body](/docs/webhooks#body)).

```go
type Event struct {
	ID        string          `json:"id"` // the same on every retry: deduplicate on it
	Type      string          `json:"type"`
	Livemode  bool            `json:"livemode"`
	CreatedAt time.Time       `json:"created_at"`
	Data      json.RawMessage `json:"data"`
}

// Payment decodes Data, for payment.* events.
func (e *Event) Payment() (*Payment, error)
```

`Data` is the [payment](#payment) for `payment.*` events (decode it with
`event.Payment()`), the [settlement](/docs/settlement#following-settlements)
for `settlement.*`, the [payout](/docs/account#the-payout-object) for
`payout.*`, and `{"message": "pong"}` for `ping`; decode the others with
`json.Unmarshal(event.Data, &yours)`. The [event types](/docs/webhooks#events):

```go
const (
	EventPaymentPartiallyPaid = "payment.partially_paid"
	EventPaymentDetecting     = "payment.detecting"
	EventPaymentConfirmed     = "payment.confirmed"
	EventPaymentUnderpaid     = "payment.underpaid"
	EventPaymentExpired       = "payment.expired"
	EventPaymentFailed        = "payment.failed"
	EventPaymentWrongAsset    = "payment.wrong_asset"
	EventPaymentLateDeposit   = "payment.late_deposit"
	EventPayoutApproved       = "payout.approved"
	EventPayoutRejected       = "payout.rejected"
	EventPayoutSent           = "payout.sent"
	EventSettlementCompleted  = "settlement.completed"
	EventSettlementFailed     = "settlement.failed"
	EventPing                 = "ping"
)
```

## Examples

### Your own payment page

Show the coins on your own site, create the payment in the coin the customer
picks, and render the amount, address, QR code and deadline from the payment
itself ([what to show](/docs/checkout#what-to-show)). `net/http` and
`html/template`, with [`github.com/skip2/go-qrcode`](https://pkg.go.dev/github.com/skip2/go-qrcode)
for the QR code:

```go
package main

import (
	"encoding/base64"
	"errors"
	"html/template"
	"log"
	"net/http"
	"os"
	"strings"

	"github.com/skip2/go-qrcode"
	payitgo "github.com/payitgo/go-sdk"
)

var client *payitgo.Client

var networks = map[string]string{
	"tron": "Tron (TRC-20)", "ethereum": "Ethereum (ERC-20)", "bsc": "BNB Smart Chain (BEP-20)",
	"bitcoin": "Bitcoin", "litecoin": "Litecoin", "dogecoin": "Dogecoin", "xrp": "XRP Ledger",
}

var page = template.Must(template.New("pay").Funcs(template.FuncMap{
	"network": func(chain string) string {
		if n, ok := networks[chain]; ok {
			return n
		}
		return chain
	},
	"coin": func(token string) string { return strings.Split(token, "_")[0] },
}).Parse(`
{{if .Tokens}}<form method="post">{{range .Tokens}}
  <button name="token" value="{{.Token}}">{{.Token}} on {{network .Chain}}: ${{or .AmountDueUSD $.Total}}</button>
{{end}}</form>{{end}}
{{with .Payment}}
  {{if not .Livemode}}<p><b>Test payment:</b> do not send coins to this address.</p>{{end}}
  <p>Send exactly <b>{{.CryptoAmount}} {{coin .Token}}</b> on <b>{{network .Chain}}</b> to</p>
  <p><code>{{.DepositAddress}}</code></p>
  {{if .DestinationTag}}<p>Destination tag (required): <b>{{.DestinationTag}}</b></p>{{end}}
  <img src="{{$.QR}}" width="220" height="220" alt="QR code">
  <p>Pay before <time datetime="{{.ExpiresAt.Format "2006-01-02T15:04:05Z07:00"}}">{{.ExpiresAt.Format "15:04 MST"}}</time></p>
{{end}}`))

type view struct {
	Total   string
	Tokens  []payitgo.TokenInfo
	Payment *payitgo.Payment
	QR      template.URL
}

// GET /pay?order=ORD-1001 lists the coins; POST creates the payment in the chosen one.
func pay(w http.ResponseWriter, r *http.Request) {
	order, err := findOrder(r.Context(), r.URL.Query().Get("order")) // your code: ID and TotalUSD ("49.90")
	if err != nil {
		http.NotFound(w, r)
		return
	}
	if r.Method != http.MethodPost {
		tokens, err := client.Tokens.List(r.Context(), &payitgo.TokenListParams{AmountUSD: order.TotalUSD})
		if err != nil {
			http.Error(w, "please try again", http.StatusBadGateway)
			return
		}
		page.Execute(w, view{Total: order.TotalUSD, Tokens: tokens})
		return
	}
	payment, err := client.Payments.Create(r.Context(), &payitgo.PaymentParams{
		OrderID:   order.ID, // the same order again returns its open payment
		AmountUSD: order.TotalUSD,
		Token:     r.FormValue("token"),
	})
	var perr *payitgo.Error
	if errors.As(err, &perr) {
		log.Printf("payitgo %d: %s", perr.Status, perr.Message) // log it; do not show it to the customer
	}
	if err != nil {
		http.Error(w, "We could not start this payment. Please try again.", http.StatusBadGateway)
		return
	}
	renderPayment(w, payment)
}

func renderPayment(w http.ResponseWriter, p *payitgo.Payment) {
	png, err := qrcode.Encode(p.QRPayload, qrcode.Medium, 440)
	if err != nil {
		http.Error(w, "please try again", http.StatusInternalServerError)
		return
	}
	qr := template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(png))
	page.Execute(w, view{Payment: p, QR: qr})
}

func main() {
	var err error
	if client, err = payitgo.NewClient(os.Getenv("PAYITGO_KEY")); err != nil {
		log.Fatal(err)
	}
	http.HandleFunc("/pay", pay)
	log.Fatal(http.ListenAndServe(":8080", nil))
}
```

A payment keeps its coin while it is open: if the customer goes back and picks
another coin, `Payments.Create` for the same order returns the first payment
unchanged. To let the customer choose on your page **after** the payment
exists, create it without `Token` and set the coin with `SelectToken`:

```go
payment, err := client.Payments.Create(ctx, &payitgo.PaymentParams{OrderID: order.ID, AmountUSD: order.TotalUSD})
if err != nil {
	return err
}
// render payment.TokenOptions (each with Token, Chain, MinUSD, AmountDueUSD, FeeNote)…
// …and when the customer picks one:
chosen, err := client.Payments.SelectToken(ctx, payment.ID, pickedToken)
if err != nil {
	return err
}
renderPayment(w, chosen)
```

To follow the payment live on the page, connect the page's script to the
socket's default namespace (`/`) and `subscribe` with `payment.CheckoutToken`
(with an acknowledgement): every `payment:update` then carries the whole
payment ([follow it live](/docs/checkout#follow-it-live)). No polling.
Fulfil the order from the webhook, never from the page.

### A webhook (IPN) handler with net/http

Read the raw body, verify it, handle each event once, and answer `2xx` within
10 seconds ([answering and retries](/docs/webhooks#answering-and-retries)):

```go
package main

import (
	"io"
	"log"
	"net/http"
	"os"

	payitgo "github.com/payitgo/go-sdk"
)

var client *payitgo.Client

func ipn(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		w.WriteHeader(http.StatusMethodNotAllowed)
		return
	}
	body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20)) // the raw bytes
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	event, err := client.Webhooks.ConstructEvent(body, r.Header.Get("X-Webhook-Signature"))
	if err != nil {
		w.WriteHeader(http.StatusBadRequest) // not from payitgo, changed, or too old
		return
	}

	ctx := r.Context()
	done, err := isProcessed(ctx, event.ID)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError) // payitgo retries
		return
	}
	if done {
		w.WriteHeader(http.StatusOK) // a retry or redelivery
		return
	}

	live, err := client.IsLive(ctx) // asks payitgo once, then remembers
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError) // payitgo retries
		return
	}
	if event.Livemode == live { // ignore the other mode's events
		switch event.Type {
		case payitgo.EventPaymentConfirmed: // final: fulfil
			p, err := event.Payment()
			if err == nil {
				err = markOrderPaid(ctx, p.OrderID, p.ID) // idempotent
			}
			if err != nil {
				w.WriteHeader(http.StatusInternalServerError)
				return
			}
		case payitgo.EventPaymentExpired: // nothing arrived in time
			p, err := event.Payment()
			if err == nil {
				err = releaseOrder(ctx, p.OrderID)
			}
			if err != nil {
				w.WriteHeader(http.StatusInternalServerError)
				return
			}
		}
	}

	if err := markProcessed(ctx, event.ID); err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	w.WriteHeader(http.StatusOK)
}

func main() {
	var err error
	client, err = payitgo.NewClient(os.Getenv("PAYITGO_KEY"),
		payitgo.WithWebhookSecret(os.Getenv("PAYITGO_WEBHOOK_SECRET")), // the signing secret of the key's mode
	)
	if err != nil {
		log.Fatal(err)
	}
	http.HandleFunc("/payitgo/ipn", ipn)
	log.Fatal(http.ListenAndServe(":8080", nil))
}
```

`isProcessed`, `markProcessed`, `markOrderPaid` and `releaseOrder` are your own
code: store processed event ids in a table with a unique key, and make
fulfilment safe to run twice. A `500` makes payitgo retry the notification. More
in [handle events safely](/docs/webhooks#handle-events-safely).

### Test mode

Build everything with your test key: the same calls, simulated payments
instead of coins, and the test webhook with its own secret
([Test mode](/docs/testing)).

```go
client, err := payitgo.NewClient(os.Getenv("PAYITGO_TEST_KEY"), payitgo.WithBaseURL("https://api.payitgo.co")) // your test key
if err != nil {
	return err
}
live, err := client.IsLive(ctx)
if err != nil {
	return err
}
fmt.Println(live) // false

// Point the TEST webhook at your development server and read its secret.
hook, err := client.Webhooks.Set(ctx, "https://dev.acme.example/payitgo/ipn")
if err != nil {
	return err
}
fmt.Println(hook.Secret) // test mode's signing secret: your dev PAYITGO_WEBHOOK_SECRET

// A signed ping arrives within seconds.
if _, err := client.Webhooks.Test(ctx); err != nil {
	return err
}

// A test payment that pays itself: payment.detecting, then payment.confirmed,
// arrive at the test webhook within half a minute.
payment, err := client.Payments.Create(ctx, &payitgo.PaymentParams{
	OrderID: "TEST-1", AmountUSD: "10", Token: "USDT_TRC20", Simulate: payitgo.OutcomeConfirmed,
})
if err != nil {
	return err
}
fmt.Println(payment.Livemode, payment.Simulation.State) // false running

// Another outcome on a payment you already have:
if _, err := client.Payments.Simulate(ctx, "TEST-2", payitgo.OutcomeUnderpaid); err != nil {
	return err
}
```

Never send real coins to a test address: nothing watches it
([why](/docs/testing#never-send-real-coins-to-a-test-address)).

Check your verification against the example in
[the recipe](/docs/webhooks#the-recipe) (tolerance `0`, because the example is
old):

```go
body := []byte(`{"created_at":"2026-10-10T09:12:44.512837Z","data":{"message":"pong"},"id":"5c0e1f9a2b7d4e3c8a6f1b2d","livemode":false,"type":"ping"}`)
event, err := payitgo.ConstructEventWithTolerance(body,
	"t=1791623565,v1=1af0ba31872e3152c422edaf9442c9f0eb4ebd202beebafbaaba14f77336194b",
	"a3858862701d1ce11ab06cb5dd9bb941d4849283f81fc0c3f0db0d99a2b82d48",
	0,
)
if err != nil {
	return err
}
fmt.Println(event.Type) // ping
```

To test your handler without the network, sign a body yourself and call it
with `httptest`:

```go
func sign(body []byte, secret string, t time.Time) string {
	mac := hmac.New(sha256.New, []byte(secret))
	fmt.Fprintf(mac, "%d.", t.Unix())
	mac.Write(body)
	return fmt.Sprintf("t=%d,v1=%s", t.Unix(), hex.EncodeToString(mac.Sum(nil)))
}

func TestIPN(t *testing.T) {
	body := []byte(`{"id":"6710c3a1f2b4e5d6a7b8c9d0","type":"ping","livemode":false,"created_at":"2026-10-10T09:00:00Z","data":{"message":"pong"}}`)
	req := httptest.NewRequest(http.MethodPost, "/payitgo/ipn", bytes.NewReader(body))
	req.Header.Set("X-Webhook-Signature", sign(body, os.Getenv("PAYITGO_WEBHOOK_SECRET"), time.Now()))
	rec := httptest.NewRecorder()
	ipn(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("status %d", rec.Code)
	}
}
```

Going live: put the live key and the **live** webhook secret in your
production environment, set the live webhook URL with the live key, and keep
the `event.Livemode` check in your handler ([going live](/docs/testing#going-live)).
