Help us shape the future of Go! Take the 2026 Go Developer Survey

balikobot

package module
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Oct 13, 2026 License: MIT Imports: 27 Imported by: 0

README

balikobot-go

A zero-dependency Go client for the Balíkobot shipping API v2. The module uses only the Go standard library and the balikobot package. The client covers branches, packages, labels, tracking, pickup orders and account capabilities, including batch and streaming label export, tracking webhook parsing, an optional in-memory discovery cache and opt-in retries for read-only calls.

Install

go get github.com/m1chlcz/balikobot-go

Go 1.27 or later is required. Only a loopback test server may use the http scheme; every other base URL must use https.

Quick start

Create a client with your API user and API key. The client sends the credentials as HTTP Basic authentication. Then add a package, get a label URL and read the tracking status.

package main

import (
	"context"
	"log"
	"time"

	balikobot "github.com/m1chlcz/balikobot-go"
	"github.com/m1chlcz/balikobot-go/carrier"
	"github.com/m1chlcz/balikobot-go/country"
	"github.com/m1chlcz/balikobot-go/currency"
)

func main() {
	client, err := balikobot.New(balikobot.Config{
		User:    "api-user",
		APIKey:  "api-key",
		Timeout: 15 * time.Second,
	})
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	result, err := client.AddPackage(ctx, carrier.PPL, balikobot.AddPackageRequest{
		EID:         "order-2026-000123-S1",
		ServiceType: "1",
		RecName:     "Example Recipient",
		RecStreet:   "Example 1",
		RecCity:     "Praha",
		RecZip:      "11000",
		RecCountry:  country.CZ,
		RecPhone:    "+420777000000",
		WeightKG:    1.5,
		LengthCM:    30,
		WidthCM:     20,
		HeightCM:    10,
		Price:       1000,
		CODCurrency: currency.CZK,
	})
	if err != nil {
		log.Fatal(err)
	}

	labelURL, err := client.Labels(ctx, carrier.PPL, result.PackageID)
	if err != nil {
		log.Fatal(err)
	}
	log.Print(labelURL)

	status, err := client.TrackStatus(ctx, carrier.PPL, result.CarrierID)
	if err != nil {
		log.Fatal(err)
	}
	log.Print(status.StatusText)
}

Enums

Carrier, currency and country values are typed, not plain strings. Three subpackages hold the types:

Package Type Format Common constants
carrier carrier.Code ^[a-z0-9]{2,32}$ PPL, DPD, DPDCZ, DPDSK, GEIS, GLS, INTIME, CP, CESKAPOSTA, BALIKOVNA, ZASILKOVNA, SP, ULOZENKA
currency currency.Code ISO 4217 ^[A-Z]{3}$ CZK, EUR, USD, GBP, PLN, HUF, RON, BGN, HRK, CHF, NOK, SEK, DKK
country country.Code ISO 3166-1 alpha-2 ^[A-Z]{2}$ EU member states plus GB, CH, NO, IS, LI, UA, RS, BA, ME, MK, AL, TR, US, CA

Every type has a Valid and a String method. Use FromString for a value that has no constant. The function trims whitespace, normalizes the case and accepts any well-formed code, so custom carriers, currencies and countries work:

custom, err := carrier.FromString("MyCarrier99")
if err != nil {
	return err
}
result, err := client.AddPackage(ctx, custom, request)

The types keep the wire values compile-time safe: a function that expects a carrier.Code rejects a bare string, and a mistyped constant fails the build. The JSON form stays a plain string, so the wire contract does not change.

ADD still accepts only currency.CZK and currency.EUR as cod_currency, because the carriers require one of those two values. Other well-formed currency codes are rejected before the request.

Version 0.2.0

Version 0.2.0 changes every carrier, currency and country parameter from string to the typed codes. This is a breaking change from v0.1.0. Update every call to use the new types, for example client.Labels(ctx, carrier.PPL, packageID).

Methods

Method Endpoint Purpose
Branches GET /{carrier}/branches/... Lists the branches of a service and country
AddPackage POST /{carrier}/add Creates one package. ADD is idempotent on eid
Overview GET /{carrier}/overview Lists the packages that ORDER has not closed
Labels POST /{carrier}/labels Gets a fresh label URL for one package
DownloadBatchLabels POST /{carrier}/labels Gets and downloads the labels of one or more packages
DownloadBatchLabelsTo POST /{carrier}/labels Streams the labels of one or more packages to an io.Writer
OrderViewLabels GET /{carrier}/orderview/{order_id} Gets the label URL of a closed order
DownloadLabel GET the label URL Downloads the label body
DownloadLabelTo GET the label URL Streams the label body to an io.Writer
TrackStatus POST /{carrier}/trackstatus Reads the tracking status of one package
ParseTrackingNotifications none Parses a tracking webhook body into typed notifications
OrderBatch POST /{carrier}/order Hands one package to the carrier batch
DropPackage POST /{carrier}/drop Removes one package before ORDER
OrderPickup POST /{carrier}/orderpickup Books one physical collection
WhoAmI GET /info/whoami Reads the account and carrier data
ActivatedServices GET /{carrier}/activatedservices Lists the activated services
Countries GET /{carrier}/countries4service Lists the destination countries
COD GET /{carrier}/cod4services Lists the cash-on-delivery destinations
CarrierCapabilities GET the discovery endpoints Discovers the contracted carriers and services
ResolveBranchID none Chooses the branch id or the branch zip for an ADD request

Errors

The client returns six sentinel errors. Use errors.Is to test them.

Error Meaning Action
ErrInvalidRequest The arguments are not valid. The client sent no request. Correct the input. Do not retry.
ErrRejected The provider refused the data permanently. Correct the data. Do not retry.
ErrUnavailable The provider is unavailable, or the request never left the client. Retry later.
ErrNotFound The carrier has no tracking data yet. Poll again later.
ErrAmbiguous A mutating call can have reached the provider. Reconcile with Overview. Then retry.
ErrInvalidResponse The answer violates the protocol. Inspect the provider. Do not retry blindly.

When a JSON endpoint answer carries a Retry-After header, ErrUnavailable wraps a TransientError with the RetryAfter field. Use errors.As to read it. OrderPickup classifies an HTTP 429 answer as ErrRejected. The branch, label download and capability calls return the bare ErrUnavailable.

var transient *balikobot.TransientError
if errors.As(err, &transient) && transient.RetryAfter > 0 {
	time.Sleep(transient.RetryAfter)
}

By default the client sends no automatic retry: one call is one HTTP attempt. Config.RetryPolicy opts read-only calls into bounded automatic retries, and mutating calls are never retried automatically. See Retries.

Retries

Every call performs exactly one HTTP attempt by default. Set Config.RetryPolicy to retry read-only calls:

client, err := balikobot.New(balikobot.Config{
	User:   "api-user",
	APIKey: "api-key",
	RetryPolicy: balikobot.RetryPolicy{
		MaxAttempts: 4,
		BaseBackoff: 200 * time.Millisecond,
	},
})
  • MaxAttempts is the total number of attempts, including the first. Values below two disable retries.
  • BaseBackoff is the delay before the first retry. Each following retry doubles the delay, capped at 30 seconds. Zero selects DefaultRetryBackoff. Jitter spreads every delay over a random interval from zero to the computed delay.
  • A provider Retry-After hint (TransientError.RetryAfter) is always the floor of the next delay.

The policy applies only to read-only calls: Branches, Overview, Labels, OrderViewLabels, TrackStatus, WhoAmI, ActivatedServices, Countries, COD, CarrierCapabilities and the label downloads. AddPackage, OrderBatch, DropPackage and OrderPickup always perform exactly one attempt, so an ambiguous mutation is never duplicated automatically. A streaming label download retries only while no byte has reached the writer; after a partial write the call fails and the caller decides how to recover.

Response limits

The client reads every JSON body with a hard limit of 8 MiB. Set Config.MaxResponseBytes to change the limit. Label and batch label downloads, buffered or streamed, use a fixed limit of 4 MiB. The client refuses redirects. It compares the response Content-Type with the expected media type before it decodes the body.

Capability cache

DiscoveryCache is an optional in-memory TTL cache with single-flight refresh de-duplication for discovery results. Use it explicitly around CarrierCapabilities or WhoAmI; the client never enables a cache on its own:

cache := balikobot.NewCapabilityCache(5 * time.Minute)
carriers, err := cache.Get(ctx, "account", func(ctx context.Context) ([]balikobot.Carrier, error) {
	return client.CarrierCapabilities(ctx)
})

Get runs the refresh only on a miss or an expired entry, and concurrent Gets for the same key share one refresh. Set stores a value directly and Invalidate drops an entry. The cache is a per-process accelerator only: two processes keep separate entries, so it cannot replace cross-process persistence. A database-backed or otherwise durable shared cache must stay the authoritative source, with the in-memory cache as a read-through layer in front of it. A failed refresh is not stored. Cached values are shared between Gets, so treat the returned slices and maps as read-only. NewWhoAmICache and NewDiscoveryCache[T] cover other discovery results.

Account mode

Set Config.LiveAccount to true or false to verify the account before each mutating call. The client calls WHOAMI and compares the live_account flag. A mismatch blocks the write before the client sends it. A successful result stays valid for five minutes. If Config.LiveAccount is nil, the client skips this check.

Label hosts

The client accepts label URLs only from the Balíkobot label hosts, or from the base URL origin for a loopback test server. Set Config.LabelHosts to replace the default allowlist with other hosts. A leading dot selects a subdomain suffix match; it does not match the bare domain.

Batch labels

DownloadBatchLabels requests fresh labels for one or more packages and downloads the label body in one step. A single package id returns a PDF or ZPL body; several package ids return a ZIP archive. The method uses the label host allowlist and the fixed 4 MiB label limit, and it accepts at most 1000 package ids per request.

labels, mediaType, err := client.DownloadBatchLabels(ctx, carrier.PPL, packageIDs)
if err != nil {
	return err
}
response.Header().Set("Content-Type", mediaType)
if _, err = response.Write(labels); err != nil {
	return err
}

Label streaming

DownloadLabelTo and DownloadBatchLabelsTo fetch a label URL and stream the validated body to an io.Writer without buffering it in memory:

mediaType, err := client.DownloadBatchLabelsTo(ctx, carrier.PPL, packageIDs, response)
if err != nil {
	return err
}
response.Header().Set("Content-Type", mediaType)

They keep every safety property of the buffered calls: the label host allowlist, the fixed 4 MiB label limit, the media-type check and the PDF, ZPL and ZIP magic prefix check. The prefix is validated before the first byte reaches the writer, but a cap violation or a mid-stream failure can leave partial output, so use the buffered calls when the output must be all-or-nothing. DownloadBatchLabelsTo returns the validated media type; DownloadLabelTo returns only the error, because a single label is always a PDF or ZPL body. An error returned by the writer is returned unchanged.

Tracking webhooks

ParseTrackingNotifications turns the body of a Balíkobot tracking webhook into typed notifications. It accepts a single JSON object or an array of objects. Every entry must identify the package through package_id, eid or carrier_id and carry a decimal status_id or status_id_v2 together with a status description in name or status_text. A malformed body or a missing required field returns ErrInvalidResponse.

notifications, err := balikobot.ParseTrackingNotifications(body)
if err != nil {
	return err
}
for _, notification := range notifications {
	log.Printf(
		"%s: %s %s",
		notification.CarrierID, notification.StatusID, notification.StatusText,
	)
}

Development

Run the checks from the module root:

gofmt -l .
go vet ./...
go test -race ./...

The tests use httptest servers. They use no real credentials and no external network.

Support

If this project helps you, you can support its development.

Support me on Ko-fi Buy me a coffee

License

MIT. See LICENSE.

Documentation

Overview

Package balikobot is a zero-dependency client for the Balíkobot shipping API v2.

The client covers the documented endpoints for carrier branches, packages, labels, tracking, pickup orders and account capabilities. It performs no logging or persistence, and every call performs exactly one HTTP attempt by default. Config.RetryPolicy opts read-only calls into automatic retries while mutating calls never retry automatically. Every method maps the provider answer to a small set of sentinel errors, so the caller decides how to react to a retryable outage or a permanent rejection.

DiscoveryCache is an explicit in-memory accelerator for carrier and account discovery results. It is never enabled implicitly, and a durable cross-process store stays the authoritative source.

Requests use HTTP Basic authentication: Config.User is the API user and Config.APIKey is the API password. All JSON responses are read with a hard byte limit and every request carries the caller's context.

Index

Constants

View Source
const (
	// DefaultBaseURL is the production Balíkobot API v2 endpoint.
	DefaultBaseURL = "https://apiv2.balikobot.cz"
	// DefaultTimeout is the request timeout used when Config.Timeout is zero.
	DefaultTimeout = 30 * time.Second
	// DefaultMaxResponseBytes is the JSON response size limit used when
	// Config.MaxResponseBytes is zero.
	DefaultMaxResponseBytes = 8 << 20
)
View Source
const DefaultRetryBackoff = 200 * time.Millisecond

DefaultRetryBackoff is the delay before the first retry when RetryPolicy.BaseBackoff is zero.

Variables

View Source
var (

	// ErrUnavailable reports that the provider is temporarily unavailable or
	// that the request never left the client. A retry can succeed. A
	// TransientError wraps this sentinel when the provider sent a retry hint.
	ErrUnavailable = errors.New("balikobot: temporarily unavailable")
	// ErrNotFound reports a documented "no data yet" answer, currently the
	// missing tracking data of a package the carrier has not scanned yet.
	ErrNotFound = errors.New("balikobot: resource not found")
	// ErrInvalidResponse reports a provider answer that violates the protocol,
	// for example a broken body, a missing status or a foreign label URL.
	ErrInvalidResponse = errors.New("balikobot: invalid provider response")
	// ErrInvalidRequest reports arguments rejected locally before any network
	// call, for example an invalid carrier code or a missing recipient field.
	ErrInvalidRequest = errors.New("balikobot: invalid request")
	// ErrRejected reports a permanent refusal of the request or of the
	// supplied data. Retrying the same request cannot succeed.
	ErrRejected = errors.New("balikobot: provider permanently rejected the request")
	// ErrAmbiguous reports that a mutating call may have reached the provider.
	// Reconcile the result by external reference before any retry.
	ErrAmbiguous = errors.New("balikobot: request outcome is unknown")
)

Functions

func ResolveBranchID

func ResolveBranchID(carrierCode carrier.Code, service, branchID, branchZip string) string

ResolveBranchID derives the branch_id that ADD expects from a stored branch of a carrier. Česká pošta and Slovenská pošta use the branch ZIP without spaces, the Uloženka CP_NP service does the same, PPL strips the KM prefix, and every other carrier uses the stored branch id unchanged.

Types

type ActivatedServices

type ActivatedServices struct {
	// ActiveParcel is the provider flag that marks active parcel shipping.
	// It is nil when the provider omits the flag.
	ActiveParcel *bool
	// Services lists the activated services. The Countries and COD fields are
	// empty; use CarrierCapabilities for the combined discovery.
	Services []Service
}

ActivatedServices is the ACTIVATEDSERVICES answer of one carrier.

type AddPackageRequest

type AddPackageRequest struct {
	// EID is the external package reference. It must contain 8 to 40
	// alphanumeric or dash characters.
	EID string `json:"eid"`
	// ServiceType is the carrier service code, for example "1" or "VMCZ".
	ServiceType string `json:"service_type"`
	// RecName is the recipient name. RecName or RecFirm must be set.
	RecName string `json:"rec_name,omitempty"`
	// RecFirm is the recipient company. RecName or RecFirm must be set.
	RecFirm string `json:"rec_firm,omitempty"`
	// RecStreet is the recipient street. It is required.
	RecStreet string `json:"rec_street"`
	// RecCity is the recipient city. It is required.
	RecCity string `json:"rec_city"`
	// RecZip is the recipient postal code. It is required.
	RecZip string `json:"rec_zip"`
	// RecCountry is the ISO 3166-1 alpha-2 destination country. It is
	// required.
	RecCountry country.Code `json:"rec_country"`
	// RecPhone is the recipient phone. RecPhone or RecEmail must be set.
	RecPhone string `json:"rec_phone,omitempty"`
	// RecEmail is the recipient email. RecPhone or RecEmail must be set.
	RecEmail string `json:"rec_email,omitempty"`
	// BranchID is the pickup branch reference for branch delivery.
	BranchID string `json:"branch_id,omitempty"`
	// WeightKG is the package weight in kilograms. It must be positive and at
	// most 10000.
	WeightKG float64 `json:"weight"`
	// LengthCM is the package length in centimeters. It must be positive and
	// at most 1000.
	LengthCM float64 `json:"length"`
	// WidthCM is the package width in centimeters. It must be positive and at
	// most 1000.
	WidthCM float64 `json:"width"`
	// HeightCM is the package height in centimeters. It must be positive and
	// at most 1000.
	HeightCM float64 `json:"height"`
	// Price is the declared value in the currency of CODCurrency. It must not
	// be negative and at most 100000000.
	Price float64 `json:"price"`
	// CODPrice is the cash-on-delivery amount. It must not be negative and at
	// most 100000000. A positive amount requires VS.
	CODPrice float64 `json:"cod_price,omitzero"`
	// CODCurrency is the cash-on-delivery currency. Balíkobot carriers
	// validate this field even without a COD amount, so "CZK" or "EUR" is
	// required on every ADD.
	CODCurrency currency.Code `json:"cod_currency,omitempty"`
	// VS is the cash-on-delivery variable symbol. It must be set exactly when
	// CODPrice is positive and must be below 10000000000.
	VS *int64 `json:"vs,omitempty"`
}

AddPackageRequest is one package for the ADD method. The external reference EID makes ADD idempotent: a repeated request with an already stored EID returns the original record.

type AddPackageResult

type AddPackageResult struct {
	// PackageID is the Balíkobot package reference used by labels, ORDER and
	// DROP.
	PackageID string
	// CarrierID is the carrier tracking number.
	CarrierID string
	// LabelURL is the provider label URL for this package.
	LabelURL string
}

AddPackageResult is the accepted package record returned by ADD.

type Branch

type Branch struct {
	// ID is the branch identifier used by the carrier.
	ID string `json:"ID"`
	// Type is the provider branch type, for example "branch" or "box".
	Type string `json:"Type"`
	// Name is the display name. It falls back to Zip when the provider sends
	// no name.
	Name string `json:"Name"`
	// Street is the street part of the address.
	Street string `json:"Street"`
	// City is the city part of the address.
	City string `json:"City"`
	// Zip is the postal code.
	Zip string `json:"Zip"`
	// Country is the ISO 3166-1 alpha-2 country code. It can be empty when
	// the provider omits it for a domestic branch.
	Country country.Code `json:"Country"`
	// Latitude is the GPS latitude when the provider sent a valid pair.
	Latitude *float64 `json:"Latitude"`
	// Longitude is the GPS longitude when the provider sent a valid pair.
	Longitude *float64 `json:"Longitude"`
}

Branch describes one carrier branch or pickup point.

type CODCapability

type CODCapability struct {
	// Country is the ISO 3166-1 alpha-2 destination country.
	Country country.Code `json:"Country"`
	// Currency is the three-letter currency code.
	Currency currency.Code `json:"Currency"`
	// MaxAmountMinor is the maximum cash-on-delivery amount in minor units,
	// for example 149995 for 1499.95 CZK.
	MaxAmountMinor int64 `json:"MaxAmountMinor"`
}

CODCapability is one cash-on-delivery destination of a service.

type Carrier

type Carrier struct {
	// CarrierCode is the carrier code used in request paths.
	CarrierCode carrier.Code `json:"CarrierCode"`
	// Services lists the activated services of the carrier.
	Services []Service `json:"Services"`
}

Carrier aggregates the discovered services of one contracted carrier.

type Client

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

Client is a Balíkobot API v2 client. Create it with New. Every method is safe for concurrent use except Close, which must not run concurrently with other calls.

func New

func New(config Config) (*Client, error)

New validates the configuration and returns a client. The user name and the API key must not be empty, and BaseURL must be an absolute URL. The https scheme is mandatory unless the host is a loopback address, which supports local test servers.

func (*Client) ActivatedServices

func (client *Client) ActivatedServices(ctx context.Context, carrierCode carrier.Code) (ActivatedServices, error)

ActivatedServices calls the ACTIVATEDSERVICES method of one carrier and returns the normalized activated services. When the provider reports that parcel shipping is inactive, the service list is empty. The call is read-only, so Config.RetryPolicy applies to it.

func (*Client) AddPackage

func (client *Client) AddPackage(
	ctx context.Context,
	carrierCode carrier.Code,
	request AddPackageRequest,
) (AddPackageResult, error)

AddPackage calls the ADD method with one package. ADD is idempotent on EID: a repeated request with an already stored EID returns status 208 together with the original record, which maps to a successful result.

func (*Client) Branches

func (client *Client) Branches(
	ctx context.Context,
	carrierCode carrier.Code,
	service string,
	countryCode country.Code,
) ([]Branch, error)

Branches calls the BRANCHES method and returns the branches of one carrier service in one country. The route depends on the carrier: the selected carriers use the combined service and country segments, Zásilkovna uses the country-only route, and the remaining carriers use the service-only route with a client-side country filter. The call is read-only, so Config.RetryPolicy applies to it.

func (*Client) COD

func (client *Client) COD(ctx context.Context, carrierCode carrier.Code) ([]ServiceCOD, error)

COD calls the COD4SERVICES method of one carrier and returns the normalized cash-on-delivery destinations per service. Every country sent by the provider is kept. A carrier without the optional dictionary returns an empty list. The call is read-only, so Config.RetryPolicy applies to it.

func (*Client) CarrierCapabilities

func (client *Client) CarrierCapabilities(
	ctx context.Context,
	carrierScope ...[]carrier.Code,
) ([]Carrier, error)

CarrierCapabilities discovers the contracted carriers and their activated services in one run. Without an argument it discovers every carrier of the account; an explicit empty scope discovers none. At most one scope slice may be passed, and every requested carrier must belong to the account. The returned destinations are restricted to EU countries, matching the reference integration. The call is read-only, so Config.RetryPolicy applies to it.

func (*Client) Close

func (client *Client) Close()

Close releases the idle connections created by the client. It does nothing for a caller-owned Config.HTTPClient. Repeated calls are safe.

func (*Client) Countries

func (client *Client) Countries(ctx context.Context, carrierCode carrier.Code) ([]ServiceCountries, error)

Countries calls the COUNTRIES4SERVICE method of one carrier and returns the supported destination countries per service. Every country sent by the provider is kept. The call is read-only, so Config.RetryPolicy applies to it.

func (*Client) DownloadBatchLabels added in v0.3.0

func (client *Client) DownloadBatchLabels(
	ctx context.Context,
	carrierCode carrier.Code,
	packageIDs []string,
) ([]byte, string, error)

DownloadBatchLabels requests fresh labels of one or more packages that have not entered ORDER yet and downloads them in one step. A single package id returns a PDF or ZPL body; several package ids return a ZIP archive. The request accepts at most labelsPackageLimit package ids. The label URL must pass the client label allowlist, and the body is read once with the fixed label byte limit. The returned body is a copy owned by the caller. The lookup and the download are read-only, so Config.RetryPolicy applies to the call.

func (*Client) DownloadBatchLabelsTo added in v0.4.0

func (client *Client) DownloadBatchLabelsTo(
	ctx context.Context,
	carrierCode carrier.Code,
	packageIDs []string,
	w io.Writer,
) (string, error)

DownloadBatchLabelsTo requests fresh labels of one or more packages that have not entered ORDER yet and streams them to the writer. It returns the validated media type: application/pdf or application/zpl for a single package id, application/zip for several package ids. The same safety checks as DownloadBatchLabels apply: the label host allowlist, the declared media type, the matching magic prefix and the fixed 4 MiB label limit. The prefix is validated before the first byte reaches the writer, but a cap violation or a mid-stream failure can leave partial output, so callers that need all-or-nothing output must use DownloadBatchLabels. The lookup and the download are read-only, and Config.RetryPolicy retries them only while no byte has reached the writer.

func (*Client) DownloadLabel

func (client *Client) DownloadLabel(
	ctx context.Context,
	labelURL string,
) ([]byte, string, error)

DownloadLabel fetches a provider label URL server-to-server. The body is read once with a hard byte limit and validated against the declared label media type. The URL host must pass the client label allowlist. The returned body is a copy owned by the caller. The fetch is read-only, so Config.RetryPolicy applies to it.

func (*Client) DownloadLabelTo added in v0.4.0

func (client *Client) DownloadLabelTo(ctx context.Context, labelURL string, w io.Writer) error

DownloadLabelTo fetches a provider label URL server-to-server and streams the body to the writer. The same safety checks as DownloadLabel apply: the URL host must pass the client label allowlist, the declared media type must be application/pdf or application/zpl, the body must start with the matching magic prefix, the body is limited to the fixed 4 MiB label limit, and a failure is classified like the buffered call. The prefix is validated before the first byte reaches the writer, but a cap violation or a mid-stream failure can leave partial output, so callers that need all-or-nothing output must use DownloadLabel. An error returned by the writer is returned unchanged. The fetch is read-only, and Config.RetryPolicy retries it only while no byte has reached the writer.

func (*Client) DropPackage

func (client *Client) DropPackage(
	ctx context.Context,
	carrierCode carrier.Code,
	packageID string,
) error

DropPackage calls the DROP method for one package that has not entered ORDER. A body status 404 means that the package is already gone and the call succeeds. A body status 405 marks a package that was already handed to the batch and maps to ErrRejected. An ambiguous DROP answer must be reconciled through Overview before any retry.

func (*Client) Labels

func (client *Client) Labels(
	ctx context.Context,
	carrierCode carrier.Code,
	packageID string,
) (string, error)

Labels asks for a fresh aggregate label URL of one package that has not entered ORDER yet. The lookup is read-only, so Config.RetryPolicy applies to it.

func (*Client) OrderBatch

func (client *Client) OrderBatch(
	ctx context.Context,
	carrierCode carrier.Code,
	packageID string,
) (OrderResult, error)

OrderBatch calls the ORDER method, which hands one package over to the carrier batch. ORDER is idempotent on package_ids: a repeated closure of the same dataset returns status 208 with the original order id, so an idempotent retry after an ambiguous answer replays the original record instead of closing the package twice.

func (*Client) OrderPickup

func (client *Client) OrderPickup(
	ctx context.Context,
	carrierCode carrier.Code,
	request PickupRequest,
) (PickupResult, error)

OrderPickup calls the ORDERPICKUP method and books one physical collection, separately from the shipment data handover performed by ORDER. The call performs exactly one HTTP attempt. DPD and DPDCZ take the collection address from the carrier configuration; PPL also defaults its contact information to that configuration.

func (*Client) OrderViewLabels

func (client *Client) OrderViewLabels(
	ctx context.Context,
	carrierCode carrier.Code,
	orderID string,
	packageID string,
) (string, error)

OrderViewLabels retrieves the label URL of an already closed ORDER. The returned URL is accepted only when the response confirms both the requested order id and the membership of the requested package id. The call is read-only, so Config.RetryPolicy applies to it.

func (*Client) Overview

func (client *Client) Overview(
	ctx context.Context,
	carrierCode carrier.Code,
	matchEID string,
) ([]OverviewPackage, error)

Overview calls the OVERVIEW method, which lists the packages of a carrier that have not yet been closed by ORDER. matchEID names the single entry whose integrity is required for reconciliation: a malformed entry with a different EID is skipped, while a malformed matching entry fails the call. The call is read-only, so Config.RetryPolicy applies to it.

func (*Client) TrackStatus

func (client *Client) TrackStatus(
	ctx context.Context,
	carrierCode carrier.Code,
	carrierID string,
) (TrackStatusResult, error)

TrackStatus calls the TRACKSTATUS method for one carrier tracking number and returns the raw provider status. The documented response wraps per-package entries in a packages array. A package entry or HTTP answer with status 404 means that the carrier has no tracking data yet and maps to ErrNotFound. The call is read-only, so Config.RetryPolicy applies to it.

func (*Client) WhoAmI

func (client *Client) WhoAmI(ctx context.Context) (WhoAmI, error)

WhoAmI calls the WHOAMI method and returns the account information. The call is read-only, so Config.RetryPolicy applies to it.

type Config

type Config struct {
	// BaseURL is the API root, for example "https://apiv2.balikobot.cz". The
	// default is DefaultBaseURL. Only the loopback host of a test server may
	// use the http scheme.
	BaseURL string
	// User is the API user. It must not be empty.
	User string
	// APIKey is the API key used as the HTTP Basic password. It must not be
	// empty.
	APIKey string
	// HTTPClient is an optional caller-owned HTTP client. The client clones
	// it, applies Timeout and refuses redirects. Close does not close the
	// idle connections of a caller-owned client.
	HTTPClient *http.Client
	// Timeout is the whole-request timeout. Zero selects DefaultTimeout.
	Timeout time.Duration
	// MaxResponseBytes is the hard byte limit for a JSON response body. Zero
	// selects DefaultMaxResponseBytes. Label downloads use a fixed 4 MiB
	// limit.
	MaxResponseBytes int
	// LabelHosts optionally restricts label downloads to these hosts. A
	// leading dot selects a suffix match, so ".balikobot.cz" covers every
	// subdomain while "pdf.balikobot.cz" matches one host. When empty, the
	// client allows the Balíkobot label hosts, or only the BaseURL origin
	// when BaseURL is a loopback test server.
	LabelHosts []string
	// LiveAccount optionally enables account-mode verification before every
	// mutating call. When set, the client first calls WHOAMI and requires the
	// live_account flag to equal this value. A failed check blocks the write
	// before it is sent. When nil, no account-mode check is performed.
	LiveAccount *bool
	// RetryPolicy opts read-only calls into automatic retries. The zero value
	// keeps the default behavior of exactly one HTTP attempt per call. The
	// policy never applies to AddPackage, OrderBatch, DropPackage or
	// OrderPickup.
	RetryPolicy RetryPolicy
}

Config configures a Client.

type DiscoveryCache added in v0.4.0

type DiscoveryCache[T any] struct {
	// contains filtered or unexported fields
}

DiscoveryCache is an in-memory TTL cache with single-flight refresh de-duplication for discovery results, for example the CarrierCapabilities and WhoAmI answers. Create it with NewCapabilityCache, NewWhoAmICache or NewDiscoveryCache.

The cache is an explicit per-process accelerator. It cannot replace cross-process persistence: separate processes keep separate entries, and a durable shared store, for example a database-backed cache, must stay the authoritative source for the deployment. The cache performs no background eviction; the next Get replaces an expired entry.

Every Get of the same key returns the same value, including its shared slices and maps, so callers must treat cached values as read-only.

func NewCapabilityCache added in v0.4.0

func NewCapabilityCache(ttl time.Duration) *DiscoveryCache[[]Carrier]

NewCapabilityCache returns a DiscoveryCache for CarrierCapabilities results.

func NewDiscoveryCache added in v0.4.0

func NewDiscoveryCache[T any](ttl time.Duration) *DiscoveryCache[T]

NewDiscoveryCache returns a cache whose entries expire after ttl. A ttl of zero or less disables reuse: every Get refreshes the value while concurrent Gets for the same key still share one refresh.

func NewWhoAmICache added in v0.4.0

func NewWhoAmICache(ttl time.Duration) *DiscoveryCache[WhoAmI]

NewWhoAmICache returns a DiscoveryCache for WhoAmI results.

func (*DiscoveryCache[T]) Get added in v0.4.0

func (cache *DiscoveryCache[T]) Get(
	ctx context.Context,
	key string,
	refresh func(context.Context) (T, error),
) (T, error)

Get returns the cached value of key, or runs refresh and stores its result when the entry is missing or expired. Concurrent Gets for the same key share one refresh; the refresh runs with the context of the first caller, and every waiting caller receives the same value or error. A failed refresh is not stored, so the next Get resolves it again. An empty key, a nil context or a nil refresh returns ErrInvalidRequest.

func (*DiscoveryCache[T]) Invalidate added in v0.4.0

func (cache *DiscoveryCache[T]) Invalidate(key string)

Invalidate drops the cached value of key and ignores an empty key. A refresh already in flight is not cancelled: its result is returned to the callers that joined it but is not stored.

func (*DiscoveryCache[T]) Set added in v0.4.0

func (cache *DiscoveryCache[T]) Set(key string, value T)

Set stores value under key with the current time and ignores an empty key. A refresh already in flight is not cancelled; its result can overwrite a concurrent Set when it completes.

type OrderResult

type OrderResult struct {
	// OrderID is the provider batch identifier.
	OrderID string
}

OrderResult is the batch reference returned by ORDER.

type OverviewPackage

type OverviewPackage struct {
	// EID is the external package reference stored by the provider.
	EID string
	// PackageID is the Balíkobot package reference.
	PackageID string
	// CarrierID is the carrier tracking number.
	CarrierID string
	// LabelURL is the provider label URL for this package.
	LabelURL string
}

OverviewPackage is one open package entry returned by OVERVIEW.

type PickupRequest

type PickupRequest struct {
	// Date is the collection date in the canonical "2006-01-02" format.
	Date string
	// WeightKG is the total collection weight in kilograms. It must be
	// positive and at most 100000. DPD and DPDCZ send it; PPL takes the
	// weight from its carrier configuration.
	WeightKG float64
	// PackageCount is the number of packages. It must be positive and at most
	// 10000. DPD and DPDCZ send it; PPL takes it from its carrier
	// configuration.
	PackageCount int
	// Note is the optional collection note. It must contain at most 255 valid
	// characters without line breaks. DPD and DPDCZ send it as "message";
	// PPL sends it as "note".
	Note string
}

PickupRequest is one physical collection booking for ORDERPICKUP.

type PickupResult

type PickupResult struct {
	// ProviderID is the provider pickup reference. PPL returns it; DPD and
	// DPDCZ leave it empty.
	ProviderID string
	// Confirmed reports the provider confirmation. DPD and DPDCZ always
	// confirm on success.
	Confirmed bool
}

PickupResult is the confirmed collection booking.

type RetryPolicy added in v0.4.0

type RetryPolicy struct {
	// MaxAttempts is the total number of attempts, including the first.
	// Values below two disable retries.
	MaxAttempts int
	// BaseBackoff is the delay before the first retry. Every following retry
	// doubles the delay, capped at 30 seconds. Zero selects
	// DefaultRetryBackoff. A longer provider Retry-After hint always wins.
	BaseBackoff time.Duration
	// Jitter spreads every retry delay over a random interval from zero to
	// the computed delay.
	Jitter bool
}

RetryPolicy opts read-only calls into automatic retries. The zero value keeps the default behavior of exactly one HTTP attempt per call. The policy never applies to the mutating calls AddPackage, OrderBatch, DropPackage and OrderPickup; those always perform exactly one attempt.

The retried calls are Branches, Overview, Labels, OrderViewLabels, TrackStatus, WhoAmI, ActivatedServices, Countries, COD, CarrierCapabilities and the label downloads.

type Service

type Service struct {
	// Code is the provider service code.
	Code string `json:"Code"`
	// Name is the provider service name.
	Name string `json:"Name"`
	// HomeDelivery reports home delivery support. Nil means that the
	// provider did not declare the flag.
	HomeDelivery *bool `json:"HomeDelivery"`
	// BoxDelivery reports box delivery support. Nil means that the provider
	// did not declare the flag.
	BoxDelivery *bool `json:"BoxDelivery"`
	// PickupPointsDelivery reports pickup point delivery support. Nil means
	// that the provider did not declare the flag.
	PickupPointsDelivery *bool `json:"PickupPointsDelivery"`
	// Countries maps the destination country codes supported by the service.
	// CarrierCapabilities keeps only EU destinations, matching the reference
	// integration.
	Countries map[country.Code]bool `json:"Countries"`
	// COD lists the supported cash-on-delivery destinations. The combined
	// discovery leaves it empty because it does not request the optional COD
	// dictionary.
	COD []CODCapability `json:"COD"`
}

Service describes one activated carrier service.

type ServiceCOD

type ServiceCOD struct {
	// ServiceType is the provider service code.
	ServiceType string
	// Countries lists the normalized cash-on-delivery destinations.
	Countries []CODCapability
}

ServiceCOD is one entry of the COD4SERVICES answer.

type ServiceCountries

type ServiceCountries struct {
	// ServiceType is the provider service code.
	ServiceType string
	// Countries lists the destination country codes exactly as sent, with
	// surrounding whitespace trimmed and letters upper-cased.
	Countries []country.Code
}

ServiceCountries is one entry of the COUNTRIES4SERVICE answer.

type TrackStatusResult

type TrackStatusResult struct {
	// StatusID is the raw provider status code, for example "1", "1.2" or
	// "-1".
	StatusID string
	// StatusText is the provider status description.
	StatusText string
}

TrackStatusResult is the latest provider tracking status of one package.

type TrackingNotification added in v0.3.0

type TrackingNotification struct {
	// PackageID is the Balíkobot package reference. It can be empty when the
	// notification identifies the package another way.
	PackageID string
	// EID is the external package reference. It can be empty when the
	// notification identifies the package another way.
	EID string
	// CarrierID is the carrier tracking number. It can be empty when the
	// notification identifies the package another way.
	CarrierID string
	// StatusID is the raw provider status code, for example "1", "1.2" or
	// "-1". The detailed status_id_v2 value wins when both are present.
	StatusID string
	// StatusText is the provider status description.
	StatusText string
}

TrackingNotification is one package status change delivered to a Balíkobot tracking webhook. At least one of PackageID, EID and CarrierID identifies the package; StatusID and StatusText describe the new provider status.

func ParseTrackingNotifications added in v0.3.0

func ParseTrackingNotifications(body []byte) ([]TrackingNotification, error)

ParseTrackingNotifications parses the JSON body of a Balíkobot tracking webhook and returns one notification per status change. The body can be a single notification object or an array of objects; an empty array returns an empty result. Every entry must identify the package through package_id, eid or carrier_id. Every entry must also carry a decimal status_id or status_id_v2 and a status description in name or status_text. A malformed body, a missing required field or more than trackingBatchLimit entries returns ErrInvalidResponse.

type TransientError

type TransientError struct {
	// RetryAfter is the provider retry hint. A zero value means that the
	// provider sent no hint and the caller should apply its own backoff.
	RetryAfter time.Duration
}

TransientError reports a retryable provider answer and carries the optional Retry-After hint of the provider. It matches ErrUnavailable with errors.Is.

func (*TransientError) Error

func (err *TransientError) Error() string

Error implements the error interface.

func (*TransientError) Unwrap

func (err *TransientError) Unwrap() error

Unwrap returns ErrUnavailable, so errors.Is(err, ErrUnavailable) is true for every transient provider answer.

type WhoAmI

type WhoAmI struct {
	// Status is the top-level provider status.
	Status int `json:"Status"`
	// LiveAccount reports whether the credentials belong to a live account.
	// It is nil when the provider omits the flag.
	LiveAccount *bool `json:"LiveAccount"`
	// Carriers lists the carriers contracted by the account.
	Carriers []WhoAmICarrier `json:"Carriers"`
}

WhoAmI is the account information returned by the WHOAMI method.

type WhoAmICarrier

type WhoAmICarrier struct {
	// Slug is the carrier code used in request paths.
	Slug carrier.Code `json:"Slug"`
	// Name is the carrier display name. It can be empty.
	Name string `json:"Name"`
}

WhoAmICarrier is one contracted carrier of the account.

Directories

Path Synopsis
Package carrier defines the carrier codes used by the Balíkobot API.
Package carrier defines the carrier codes used by the Balíkobot API.
Package country defines ISO 3166-1 alpha-2 country codes used by the Balíkobot API.
Package country defines ISO 3166-1 alpha-2 country codes used by the Balíkobot API.
Package currency defines ISO 4217 currency codes used by the Balíkobot API.
Package currency defines ISO 4217 currency codes used by the Balíkobot API.

Jump to

Keyboard shortcuts

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