customers

package
v1.39.0 Latest Latest
Warning

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

Go to latest
Published: Nov 13, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package customers is a client for Coinbase Developer Platform's Customers API (https://docs.cdp.coinbase.com/customers-kyc/overview), through which Coinbase verifies the identity of a Flipcash user (KYC) so they can later cash out to a bank account. The server collects the user's information and passes it on server to server; Coinbase decides.

The client covers create, get, update and delete, which is all the server uses: list and the requirements preview are not built. It speaks the REST API directly with typed requests and responses (model.go), signing each request with the shared CDP JWT (package cdp).

The client never logs, and never puts a request body or any submitted value into an error: see Error for what an error carries. Timeouts and retries are the caller's, through the http.Client, the context and IsRetryable.

customerstest is an in-process fake of the API for tests.

Index

Constants

View Source
const (
	// SandboxBaseURL is the CDP Sandbox, where Coinbase's magic values
	// force each verification outcome.
	SandboxBaseURL = "https://sandbox.cdp.coinbase.com/platform"
	// ProductionBaseURL is live CDP.
	ProductionBaseURL = "https://api.cdp.coinbase.com/platform"
)

Variables

View Source
var ErrInvalidArgument = errors.New("cdp customers: invalid argument")

ErrInvalidArgument wraps every error for a request the client refused to send (a malformed customer ID or idempotency key, a tax attestation with no form). Nothing reached Coinbase.

View Source
var ErrMalformedResponse = errors.New("cdp customers: malformed response")

ErrMalformedResponse wraps the error for a successful response whose body could not be read as a customer. Whether the write it answered landed is unknown; retrying it under the same idempotency key is safe.

Functions

func HasType

func HasType(err error, t ErrorType) bool

HasType reports whether err is a Coinbase error of type t.

func IsClientError

func IsClientError(err error) bool

IsClientError reports whether err is a 4xx from Coinbase other than a rate limit, or a request the client refused to send: the request was wrong, and sending it again unchanged will fail the same way.

func IsCustomerID

func IsCustomerID(id string) bool

IsCustomerID reports whether id has the shape of a Coinbase customer ID, "customer_" followed by 36 lowercase hex digits and dashes. It judges the shape alone, not whether Coinbase knows the customer.

func IsInvalidRequest

func IsInvalidRequest(err error) bool

IsInvalidRequest reports whether err is Coinbase's generic validation failure. Error.Message says what failed.

func IsNotFound

func IsNotFound(err error) bool

IsNotFound reports whether err says the customer does not exist.

func IsRateLimited

func IsRateLimited(err error) bool

IsRateLimited reports whether err is Coinbase asking the caller to slow down. It is retryable.

func IsRetryable

func IsRetryable(err error) bool

IsRetryable reports whether err may succeed if the request is sent again (under the same idempotency key, for a create): a 5xx, a rate limit, a transport failure or timeout, or a success whose body could not be read. A caller's own cancellation is not retryable, nor is a client error.

func IsUnsupportedTosLanguage

func IsUnsupportedTosLanguage(err error) bool

IsUnsupportedTosLanguage reports whether err refused a Terms of Service acceptance for its language.

Types

type AddressInput

type AddressInput struct {
	Line1 string  `json:"line1,omitempty"`
	Line2 *string `json:"line2,omitempty"`
	City  string  `json:"city,omitempty"`
	// State is the two-letter code, "MA".
	State    string `json:"state,omitempty"`
	PostCode string `json:"postCode,omitempty"`
	// CountryCode must be "US".
	CountryCode string `json:"countryCode,omitempty"`
}

AddressInput is a US address. Unset fields are left out, except Line2, which is a pointer so that a pointer to "" sends an explicit empty second line (clearing one submitted before) while nil leaves it out.

type Capability

type Capability struct {
	Requested bool             `json:"requested"`
	Status    CapabilityStatus `json:"status"`
}

Capability is one entry of a customer's capabilities map.

type CapabilityName

type CapabilityName string

CapabilityName names a capability, something Coinbase authorizes a customer to do. The constants are the names Coinbase documents today; it may add more, and a read keeps any name it returns.

const (
	CapabilityCustodyCrypto      CapabilityName = "custodyCrypto"
	CapabilityCustodyFiat        CapabilityName = "custodyFiat"
	CapabilityCustodyStablecoin  CapabilityName = "custodyStablecoin"
	CapabilityTradeCrypto        CapabilityName = "tradeCrypto"
	CapabilityTradeStablecoin    CapabilityName = "tradeStablecoin"
	CapabilityTransferCrypto     CapabilityName = "transferCrypto"
	CapabilityTransferFiat       CapabilityName = "transferFiat"
	CapabilityTransferStablecoin CapabilityName = "transferStablecoin"
)

type CapabilityRequest

type CapabilityRequest struct {
	Requested bool `json:"requested"`
}

CapabilityRequest requests a capability (or withdraws a request).

type CapabilityStatus

type CapabilityStatus string

CapabilityStatus is where a capability stands. A read keeps any status Coinbase returns, documented or not; a caller must treat one it does not know as not active.

const (
	// CapabilityUnrequested: never requested; does nothing.
	CapabilityUnrequested CapabilityStatus = "unrequested"
	// CapabilityPending: requested, and either requirements are outstanding
	// or verification is running. A recoverable decline also stays here.
	CapabilityPending CapabilityStatus = "pending"
	// CapabilityActive: granted.
	CapabilityActive CapabilityStatus = "active"
	// CapabilityInactive: blocked by a terminal compliance decision, which
	// nothing submitted through the API can lift.
	CapabilityInactive CapabilityStatus = "inactive"
)

type Client

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

Client calls the Customers API as one CDP API key. It is safe for concurrent use.

func NewClient

func NewClient(httpClient *http.Client, baseURL string, keyID string, privateKey ed25519.PrivateKey) (*Client, error)

NewClient returns a client for the API at baseURL (SandboxBaseURL, ProductionBaseURL, or a test server's), authenticating as the CDP API key keyID / privateKey. httpClient carries the caller's timeouts and transport.

baseURL must be an absolute http(s) URL with no query, fragment or trailing slash; it is refused rather than cleaned up.

func (*Client) Create

func (c *Client) Create(ctx context.Context, idempotencyKey string, input *CustomerInput) (*Customer, error)

Create creates an individual customer from input, which may be empty (a customer with nothing requested), or request capabilities and submit identity in the same call.

idempotencyKey is required, a UUID v4 (Coinbase's documented format): a create sent again under the same key returns the customer the first one made, so a create whose response was lost can be retried safely. The same key with a different input is a 422 ErrorTypeIdempotency.

func (*Client) Delete

func (c *Client) Delete(ctx context.Context, customerID string) error

Delete deletes a customer and everything Coinbase holds for it. Deleting one already deleted succeeds; a customer that never existed is IsNotFound.

func (*Client) Get

func (c *Client) Get(ctx context.Context, customerID string) (*Customer, error)

Get reads a customer.

func (*Client) Update

func (c *Client) Update(ctx context.Context, customerID string, idempotencyKey string, input *CustomerInput) (*Customer, error)

Update applies input to a customer: what it sets is submitted, the rest is left as it was. It returns the customer after the update, whose requirements may already reflect it (a submitted field pending, the tos requirement appearing once the address is in).

idempotencyKey is optional (empty for none); when set it must be a UUID v4.

type Compliance

type Compliance struct {
	// RequesterIPAddress is the end user's IPv4 or IPv6 address (never the
	// server's): the live session's, or the last one stored for them.
	RequesterIPAddress string `json:"requesterIpAddress"`
}

Compliance is the per-request compliance context.

type Customer

type Customer struct {
	ID   string       `json:"customerId"`
	Type CustomerType `json:"type"`
	// Individual is the part of the identity Coinbase echoes back: names and
	// email alone. Nil when none of them is set.
	Individual   *Individual                    `json:"individual,omitempty"`
	Capabilities map[CapabilityName]Capability  `json:"capabilities,omitempty"`
	Requirements map[RequirementKey]Requirement `json:"requirements,omitempty"`
	CreatedAt    time.Time                      `json:"createdAt"`
	UpdatedAt    time.Time                      `json:"updatedAt"`
}

Customer is a customer as Coinbase returns it on every read and write.

There is no overall status: a customer is where its capabilities and requirements say it is. Coinbase returns neither map on a customer that has never requested a capability (observed in the Sandbox, though its spec marks both required), and no requirements map once nothing is outstanding, so either may be nil.

type CustomerInput

type CustomerInput struct {
	// Capabilities to request (or, with Requested false, to stop
	// requesting). Requesting any needs Compliance.
	Capabilities map[CapabilityName]CapabilityRequest `json:"capabilities,omitempty"`
	Individual   *IndividualInput                     `json:"individual,omitempty"`
	// TosAcceptances records accepted Terms of Service versions. An empty
	// list is never sent (Coinbase refuses one), so empty and nil both mean
	// "none submitted".
	TosAcceptances []TosAcceptance `json:"tosAcceptances,omitempty"`
	// TaxAttestations records tax attestations, as TosAcceptances: an empty
	// list is never sent.
	TaxAttestations []TaxAttestation `json:"taxAttestations,omitempty"`
	// Compliance carries the end user's IP address, required on every write
	// to a customer who has requested a capability.
	Compliance *Compliance `json:"compliance,omitempty"`
}

CustomerInput is what a create or update submits. Every field is optional: an update changes what it sets and leaves the rest as it was.

Values are sent as given. The client does not check them against Coinbase's schema (formats, lengths, enums); Coinbase does, with a 400 invalid_request whose message names the field.

type CustomerType

type CustomerType string

CustomerType is the kind of customer. Coinbase supports individuals alone, and so does this client: Create always sends CustomerTypeIndividual.

const (
	CustomerTypeIndividual CustomerType = "individual"
)

type DateOfBirth

type DateOfBirth struct {
	Day   string `json:"day"`
	Month string `json:"month"`
	Year  string `json:"year"`
}

DateOfBirth is Coinbase's date of birth: three decimal strings. Day and month may be zero-padded or not; year is four digits.

type Error

type Error struct {
	// StatusCode is the HTTP status.
	StatusCode int
	// Type is Coinbase's `errorType`, empty when the body was not a
	// Coinbase error (a proxy's 502 page, say).
	Type ErrorType
	// Message is Coinbase's `errorMessage`. May contain submitted values.
	Message string
	// CorrelationID is Coinbase's `correlationId`, for their support.
	CorrelationID string
}

Error is a response from Coinbase other than success.

Error() names the status, the type and the correlation ID and nothing else. Message is Coinbase's human-readable detail, which can quote values that were submitted (a name, an SSN's shape, a Terms of Service version), so it is kept apart for the caller to decide whether to log; it is never part of Error().

func AsError

func AsError(err error) (*Error, bool)

AsError returns err's *Error, if it is (or wraps) one.

func (*Error) Error

func (e *Error) Error() string

type ErrorType

type ErrorType string

ErrorType is Coinbase's machine-readable error code (`errorType`). The constants are the ones a customers caller branches on; any other value Coinbase returns is kept on Error.Type as is.

const (
	// ErrorTypeInvalidRequest: the request failed validation (a field's
	// format, a Terms of Service version the customer is not required to
	// accept, a missing requester IP, ...). Error.Message says which, and
	// may quote what was submitted.
	ErrorTypeInvalidRequest ErrorType = "invalid_request"
	// ErrorTypeUnsupportedTosLanguage: an acceptance named a language its
	// Terms of Service version is not published in.
	ErrorTypeUnsupportedTosLanguage ErrorType = "unsupported_tos_language"
	// ErrorTypeNotFound: no such customer (or it was deleted).
	ErrorTypeNotFound ErrorType = "not_found"
	// ErrorTypeRateLimitExceeded: slow down and retry.
	ErrorTypeRateLimitExceeded ErrorType = "rate_limit_exceeded"
	// ErrorTypeIdempotency: the idempotency key was used before with a
	// different request.
	ErrorTypeIdempotency ErrorType = "idempotency_error"
	// ErrorTypeUnauthorized: the bearer JWT was refused.
	ErrorTypeUnauthorized ErrorType = "unauthorized"
)

type Individual

type Individual struct {
	FirstName string `json:"firstName,omitempty"`
	LastName  string `json:"lastName,omitempty"`
	Email     string `json:"email,omitempty"`
}

Individual is the identity Coinbase returns on a read. Everything else submitted (SSN, date of birth, address, phone, due-diligence answers, terms acceptances, tax attestations, the requester's IP) is write-only.

type IndividualInput

type IndividualInput struct {
	FirstName   string        `json:"firstName,omitempty"`
	LastName    string        `json:"lastName,omitempty"`
	DateOfBirth *DateOfBirth  `json:"dateOfBirth,omitempty"`
	Address     *AddressInput `json:"address,omitempty"`
	// FullSsn is the full SSN, "123-45-6789".
	FullSsn string `json:"fullSsn,omitempty"`
	Email   string `json:"email,omitempty"`
	// PhoneNumber is E.164, "+15551234567".
	PhoneNumber      string `json:"phoneNumber,omitempty"`
	PurposeOfAccount string `json:"purposeOfAccount,omitempty"`
	SourceOfFunds    string `json:"sourceOfFunds,omitempty"`
}

IndividualInput is the identity a write submits. Unset fields (the empty string, a nil pointer) are left out of the request and so left unchanged.

type Requirement

type Requirement struct {
	Status RequirementStatus `json:"status"`
	// Deadline is when the requirement must be met, if Coinbase set one. For
	// RequirementTos it is the earliest deadline among TosVersions; read
	// each version's own.
	Deadline *time.Time `json:"deadline,omitempty"`
	// Impact lists the capabilities this requirement blocks, sorted.
	Impact []CapabilityName `json:"impact,omitempty"`
	// TosVersions is set on RequirementTos alone: the Terms of Service
	// versions the customer has yet to accept.
	TosVersions []TermsOfService `json:"tosVersions,omitempty"`
	// TaxForms is set on RequirementTaxAttestation alone: the forms the
	// customer has yet to attest to.
	TaxForms []TaxForm `json:"taxForms,omitempty"`
}

Requirement is one entry of a customer's requirements map.

type RequirementKey

type RequirementKey string

RequirementKey names an outstanding requirement: an identity field (firstName, addressCity, ...), or one of the two non-field requirements, RequirementTos and RequirementTaxAttestation. The constants are the keys Coinbase documents today; a read keeps any key it returns.

const (
	RequirementFirstName          RequirementKey = "firstName"
	RequirementLastName           RequirementKey = "lastName"
	RequirementDateOfBirth        RequirementKey = "dateOfBirth"
	RequirementSsnLast4           RequirementKey = "ssnLast4"
	RequirementFullSsn            RequirementKey = "fullSsn"
	RequirementAddressLine1       RequirementKey = "addressLine1"
	RequirementAddressLine2       RequirementKey = "addressLine2"
	RequirementAddressCity        RequirementKey = "addressCity"
	RequirementAddressSubdivision RequirementKey = "addressSubdivision"
	RequirementAddressPostalCode  RequirementKey = "addressPostalCode"
	RequirementAddressCountryCode RequirementKey = "addressCountryCode"
	RequirementEmail              RequirementKey = "email"
	RequirementPhoneNumber        RequirementKey = "phoneNumber"
	RequirementCitizenship        RequirementKey = "citizenship"
	RequirementPurposeOfAccount   RequirementKey = "purposeOfAccount"
	RequirementSourceOfFunds      RequirementKey = "sourceOfFunds"
	RequirementEmploymentStatus   RequirementKey = "employmentStatus"
	RequirementOccupation         RequirementKey = "occupation"
	RequirementExpectedVolume     RequirementKey = "expectedVolume"
	RequirementTos                RequirementKey = "tos"
	RequirementTaxAttestation     RequirementKey = "taxAttestation"
)

type RequirementStatus

type RequirementStatus string

RequirementStatus is where a requirement stands. A satisfied requirement is not listed at all. A read keeps any status Coinbase returns.

const (
	// RequirementDue: must be submitted (no deadline, or one still ahead).
	RequirementDue RequirementStatus = "due"
	// RequirementPastDue: must be submitted and its deadline has passed.
	RequirementPastDue RequirementStatus = "past_due"
	// RequirementPending: submitted and being verified.
	RequirementPending RequirementStatus = "pending"
	// RequirementRejected: the submitted value failed verification; collect
	// it again and resubmit.
	RequirementRejected RequirementStatus = "rejected"
)

type TaxAttestation

type TaxAttestation struct {
	UsW9 *UsW9Attestation
}

TaxAttestation is one tax attestation. Coinbase defines one form today, so exactly one variant must be set (W-9); a write with none or several is refused before it is sent.

func (TaxAttestation) MarshalJSON

func (a TaxAttestation) MarshalJSON() ([]byte, error)

MarshalJSON writes the attestation in Coinbase's shape, its form named by the `form` discriminator.

type TaxForm

type TaxForm string

TaxForm names a tax form a customer may have to attest to.

const (
	// TaxFormUsW9 is IRS Form W-9.
	TaxFormUsW9 TaxForm = "us_w9"
)

type TermsOfService

type TermsOfService struct {
	// VersionID is opaque; accept it verbatim.
	VersionID string `json:"versionId"`
	// Languages are the BCP 47 tags the version is published in. An
	// acceptance must name one of them.
	Languages []string `json:"languages"`
	// URL is the document, language-agnostic; append ?lang=<tag> for a
	// translation.
	URL string `json:"url"`
	// Deadline, when set, ends a grace period: before it the requirement is
	// due but not blocking, after it past_due.
	Deadline *time.Time `json:"deadline,omitempty"`
}

TermsOfService is one Terms of Service version a customer must accept.

type TosAcceptance

type TosAcceptance struct {
	VersionID  string    `json:"versionId"`
	Language   string    `json:"language"`
	AcceptedAt time.Time `json:"acceptedAt"`
}

TosAcceptance records that the customer accepted a Terms of Service version, shown to them in Language, at AcceptedAt (which Coinbase refuses if it lies in the future).

type UsW9Attestation

type UsW9Attestation struct {
	IsExemptBackupWithholding bool
	EdeliveryConsent          bool
	AcceptedAt                time.Time
}

UsW9Attestation is the customer's certification of IRS Form W-9 as of AcceptedAt. It certifies their name and SSN: changing either afterwards, without a new W-9 in the same write, voids it.

Directories

Path Synopsis
Package customerstest is an in-process fake of Coinbase's Customers API for tests: an httptest server speaking the same REST API as Coinbase, so tests drive the real customers.Client against it rather than a mock.
Package customerstest is an in-process fake of Coinbase's Customers API for tests: an httptest server speaking the same REST API as Coinbase, so tests drive the real customers.Client against it rather than a mock.

Jump to

Keyboard shortcuts

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