Documentation
¶
Overview ¶
Package primitive provides a small, high-level inbound/outbound email SDK for Primitive.
The main surface is centered around Receive for inbound webhooks and Client for outbound send/reply/forward operations.
Lower-level webhook and generated API helpers still remain available for advanced use cases.
Import the module path github.com/primitivedotdev/sdks/sdk-go and use the package name primitive in code.
For lower-level use cases, applications can call HandleWebhook, VerifyWebhookSignature, ParseWebhookEvent, or ValidateEmailReceivedEvent directly, or use the generated API client in the sibling api package.
Unknown future event types are preserved as UnknownEvent values so consumers can continue receiving webhook traffic before a package update ships.
Index ¶
- Constants
- Variables
- func BuildForwardSubject(subject string) string
- func BuildPayoutRegistrationMessage(org, address, network, issuedAt string) string
- func BuildReplySubject(subject string) string
- func ComputePaymentValidityWindow(input ValidityWindowInput) (validAfter, validBefore *big.Int, err error)
- func ConfirmedHeaders() map[string]string
- func DecodeRawEmail(event any, verify ...bool) ([]byte, error)
- func DeriveEIP3009Nonce(input NonceBinding) (string, error)
- func FormatAddress(address ReceivedEmailAddress) string
- func GetDownloadTimeRemaining(event any, nowMillis ...int64) (int64, error)
- func IsDownloadExpired(event any, nowMillis ...int64) (bool, error)
- func IsEmailReceivedEvent(event any) bool
- func IsEmailReceivedEventType(event any) bool
- func IsInteractionX402Event(event any) bool
- func IsKnownWebhookEventType(eventType string) bool
- func IsPaymentEvent(event any) bool
- func IsPaymentFailedEvent(event any) bool
- func IsPaymentSettledEvent(event any) bool
- func IsRawIncluded(event any) (bool, error)
- func ParseJSONBody(rawBody any) (any, error)
- func PrepareStandardWebhooksSecret(secret any) ([]byte, error)
- func VerifyRawEmailDownload(downloaded []byte, event any) ([]byte, error)
- func VerifyStandardWebhooksSignature(options StandardWebhooksVerifyOptions) (bool, error)
- func VerifyWebhookSignature(options VerifyOptions) (bool, error)
- type APIError
- type AuthConfidence
- type AuthVerdict
- type BounceAnalysis
- type BuildPaymentStepEnvelopeInput
- type BuiltPaymentStep
- type Client
- func (c *Client) Forward(ctx context.Context, email *ReceivedEmail, input ForwardParams) (SendResult, error)
- func (c *Client) Reply(ctx context.Context, email *ReceivedEmail, input ReplyParams) (SendResult, error)
- func (c *Client) SemanticSearch(ctx context.Context, request *primitiveapi.SemanticSearchInput) (SemanticSearchResponse, error)
- func (c *Client) Send(ctx context.Context, params SendParams) (SendResult, error)
- type ClientOptions
- type DKIMSignature
- type DMARCPolicyPublished
- type DMARCRecord
- type DMARCReportAnalysis
- type Delivery
- type DkimResult
- type DmarcPolicy
- type DmarcResult
- type DownloadInfo
- type Email
- type EmailAddress
- type EmailAnalysis
- type EmailAuth
- type EmailContent
- type EmailHeaders
- type EmailReceivedEvent
- type ErrorDefinition
- type EventType
- type ForwardAnalysis
- type ForwardOriginalSender
- type ForwardParams
- type ForwardResult
- type ForwardVerdict
- type ForwardVerification
- type HandleWebhookOptions
- type InteractionEnvelope
- type InteractionEvent
- type NonceBinding
- type ParsedData
- type ParsedError
- type ParsedStatus
- type PaymentEvent
- type PrimitiveWebhookError
- func (e *PrimitiveWebhookError) As(target any) bool
- func (e *PrimitiveWebhookError) Code() string
- func (e *PrimitiveWebhookError) Error() string
- func (e *PrimitiveWebhookError) Message() string
- func (e *PrimitiveWebhookError) Name() string
- func (e *PrimitiveWebhookError) Suggestion() string
- func (e *PrimitiveWebhookError) ToMap() map[string]any
- func (e *PrimitiveWebhookError) Unwrap() error
- type PrivateKeySigner
- type RawContent
- type RawEmailDecodeError
- type ReceiveRequestOptions
- type ReceivedEmail
- type ReceivedEmailAddress
- type ReceivedEmailThread
- type ReplyParams
- type ReportDateRange
- type SMTPEnvelope
- type SemanticSearchResponse
- type SendAttachment
- type SendParams
- type SendResult
- type SendThread
- type SignInteractionPaymentInput
- type SignResult
- type SpamAssassinAnalysis
- type SpfResult
- type StandardWebhooksSignResult
- type StandardWebhooksVerifyOptions
- type TLSReportAnalysis
- type TLSReportFailure
- type TLSReportPolicy
- type TokenDomain
- type TransferAuthorization
- type TrustReason
- type TrustedSenderOptions
- type TrustedSenderResult
- type UnknownEvent
- type ValidateEmailAuthResult
- type ValidationIssue
- type ValidationResult
- type ValidityWindowInput
- type VerifyOptions
- type WebhookAttachment
- type WebhookEvent
- type WebhookPayloadError
- type WebhookValidationError
- type WebhookVerificationError
- type X402Challenge
- type X402ChargeInput
- type X402Client
- func (c *X402Client) Charge(ctx context.Context, input X402ChargeInput) (*X402Challenge, error)
- func (c *X402Client) CreateEmailChallenge(ctx context.Context, input X402EmailChargeInput) (*X402EmailChallenge, error)
- func (c *X402Client) GetChallenge(ctx context.Context, id string) (*X402Challenge, error)
- func (c *X402Client) GetSpendPolicy(ctx context.Context) (*X402SpendPolicy, error)
- func (c *X402Client) ListDeclinedPayments(ctx context.Context) ([]X402DeclinedPayment, error)
- func (c *X402Client) ListPayoutAddresses(ctx context.Context) ([]X402PayoutAddress, error)
- func (c *X402Client) Pay(ctx context.Context, challenge *X402Challenge, signer X402Signer) (*X402Receipt, error)
- func (c *X402Client) PayEmailChallenge(challenge *X402EmailChallenge, signer X402Signer) (*BuiltPaymentStep, error)
- func (c *X402Client) RegisterPayoutAddress(ctx context.Context, input X402PayoutRegistrationInput, signer X402Signer) (*X402PayoutAddress, error)
- func (c *X402Client) SetSpendPolicy(ctx context.Context, update X402SpendPolicyUpdate) (*X402SpendPolicy, error)
- type X402ClientOptions
- type X402DeclinedPayment
- type X402EmailChallenge
- type X402EmailChallengeDetails
- type X402EmailChargeInput
- type X402Error
- type X402NonceBinding
- type X402PaymentPayload
- type X402PaymentRequirements
- type X402PaymentStepPayload
- type X402PayoutAddress
- type X402PayoutRegistrationInput
- type X402Receipt
- type X402Signer
- type X402SpendPolicy
- type X402SpendPolicyUpdate
- func (u *X402SpendPolicyUpdate) ClearMaxPerDay() *X402SpendPolicyUpdate
- func (u *X402SpendPolicyUpdate) ClearMaxPerPayment() *X402SpendPolicyUpdate
- func (u *X402SpendPolicyUpdate) SetAllowlist(v []string) *X402SpendPolicyUpdate
- func (u *X402SpendPolicyUpdate) SetMaxPerDay(v string) *X402SpendPolicyUpdate
- func (u *X402SpendPolicyUpdate) SetMaxPerPayment(v string) *X402SpendPolicyUpdate
- func (u *X402SpendPolicyUpdate) SetPaused(v bool) *X402SpendPolicyUpdate
Constants ¶
const ( WebhookVersion = "2025-12-14" PrimitiveSignatureHeader = "Primitive-Signature" LegacySignatureHeader = "MyMX-Signature" PrimitiveConfirmedHeader = "X-Primitive-Confirmed" LegacyConfirmedHeader = "X-MyMX-Confirmed" // WebhookEventHeader names the webhook event for ALL event families // (email.*, payment.*, interaction.*). It is the primary discriminator the // parser keys on, because the stored body is sent verbatim with no envelope. WebhookEventHeader = "X-Webhook-Event" StandardWebhookIDHeader = "webhook-id" StandardWebhookTimestampHeader = "webhook-timestamp" StandardWebhookSignatureHeader = "webhook-signature" )
const ( X402InteractionProtocol = "x402.payment" X402InteractionProtocolVersion = 1 )
X402InteractionProtocol / X402InteractionProtocolVersion identify the protocol the email-native payment interaction runs (x402.payment/1). The payer's reply carries the payment step of this protocol.
const DefaultMaxWindowSec int64 = 24 * 60 * 60
DefaultMaxWindowSec is the absolute ceiling on the total signed window (validBefore - validAfter). A signed EIP-3009 authorization stays settleable on-chain until validBefore regardless of the interaction state, so an unbounded window is a standing "funds committed" risk. The real window is minutes; this 24h cap is the hard safety ceiling, enforced so a caller-supplied window cannot bypass it.
const DefaultMinSettlementHeadroomSec int64 = 60
DefaultMinSettlementHeadroomSec is the minimum headroom between now and validBefore. The platform rejects a payment whose authorization is about to expire (it needs SMTP + DKIM + verify + settle latency to clear), so a validBefore less than this far in the future is a guaranteed-to-fail signature. The default window is minutes; this 60s floor is the absolute minimum the band tolerates.
const DefaultX402BaseURL = "https://api.primitive.dev"
DefaultX402BaseURL is the production API host for x402 operations. Mirrors the Node SDK's DEFAULT_BASE_URL.
Variables ¶
var EmailEventTypes = []string{
"email.received",
"email.bounced",
"email.tls_report",
"email.dmarc_report",
"email.dmarc_failure",
}
EmailEventTypes are the five first-party email events (subject = an email).
var EmailReceivedEventJSONSchema map[string]any
var InteractionEventTypes = []string{
"interaction.ack.acked",
"interaction.ack.canceled",
"interaction.ack.expired",
"interaction.ack.received",
"interaction.ack.requested",
"interaction.x402.challenge",
"interaction.x402.declined",
"interaction.x402.expired",
"interaction.x402.payment",
"interaction.x402.rejected",
"interaction.x402.settled",
"interaction.x402.verify_timeout",
}
InteractionEventTypes are the interaction step events (subject = an interaction), named interaction.<protocolShort>.<suffix>.
var PayloadErrors = map[string]ErrorDefinition{
"PAYLOAD_NULL": {
Message: "Webhook payload is null",
Suggestion: "Ensure you're passing the parsed JSON body, not null. Check your framework's body parsing middleware.",
},
"PAYLOAD_UNDEFINED": {
Message: "Webhook payload is undefined",
Suggestion: "The payload was not provided. Make sure you're passing the request body to the handler.",
},
"PAYLOAD_WRONG_TYPE": {
Message: "Webhook payload must be an object",
Suggestion: "The payload should be a parsed JSON object. Check that you're not passing a string or other primitive.",
},
"PAYLOAD_IS_ARRAY": {
Message: "Webhook payload is an array, expected object",
Suggestion: "Primitive webhooks are single event objects, not arrays. Check the payload structure.",
},
"PAYLOAD_MISSING_EVENT": {
Message: "Webhook payload missing 'event' field",
Suggestion: "All webhook payloads must have an 'event' field. This may not be a valid Primitive webhook.",
},
"PAYLOAD_UNKNOWN_EVENT": {
Message: "Unknown webhook event type",
Suggestion: "This event type is not recognized. You may need to update your SDK or handle unknown events gracefully.",
},
"PAYLOAD_EMPTY_BODY": {
Message: "Request body is empty",
Suggestion: "The request body was empty. Ensure the webhook is sending data and your framework is parsing it correctly.",
},
"JSON_PARSE_FAILED": {
Message: "Failed to parse JSON body",
Suggestion: "The request body is not valid JSON. Check the raw body content and Content-Type header.",
},
"INVALID_ENCODING": {
Message: "Invalid body encoding",
Suggestion: "The request body encoding is not supported. Primitive webhooks use UTF-8 encoded JSON.",
},
}
var PaymentEventTypes = []string{
"payment.settled",
"payment.failed",
}
PaymentEventTypes are the two x402 settlement-notification events (subject = a payment).
var RawEmailErrors = map[string]ErrorDefinition{
"NOT_INCLUDED": {
Message: "Raw email content not included inline",
Suggestion: "Use the download URL at event.email.content.download.url to fetch the raw email.",
},
"INVALID_BASE64": {
Message: "Raw email content is not valid base64",
Suggestion: "The raw email data is malformed. Fetch the raw email from the download URL or regenerate the webhook payload.",
},
"HASH_MISMATCH": {
Message: "SHA-256 hash verification failed",
Suggestion: "The raw email data may be corrupted. Try downloading from the URL instead.",
},
}
var VerificationErrors = map[string]ErrorDefinition{
"INVALID_SIGNATURE_HEADER": {
Message: "Missing or malformed Primitive-Signature header",
Suggestion: "Check that you're reading the correct header (Primitive-Signature) and it's being passed correctly from your web framework.",
},
"TIMESTAMP_OUT_OF_RANGE": {
Message: "Timestamp is too old (possible replay attack)",
Suggestion: "This could indicate a replay attack, network delay, or server clock drift. Check your server's time is synced.",
},
"SIGNATURE_MISMATCH": {
Message: "Signature doesn't match expected value",
Suggestion: "Verify the webhook secret matches and you're using the raw request body (not re-serialized JSON).",
},
"MISSING_SECRET": {
Message: "No webhook secret was provided",
Suggestion: "Pass your webhook secret from the Primitive dashboard. Check that the environment variable is set.",
},
}
var WebhookEventTypes = func() []string { all := make([]string, 0, len(EmailEventTypes)+len(PaymentEventTypes)+len(InteractionEventTypes)) all = append(all, EmailEventTypes...) all = append(all, PaymentEventTypes...) all = append(all, InteractionEventTypes...) return all }()
WebhookEventTypes is the full enumerated catalog of every current webhook event type: the five email.*, the two payment.*, and every interaction.<protocol>.<suffix>.
Functions ¶
func BuildForwardSubject ¶ added in v0.8.0
func BuildPayoutRegistrationMessage ¶ added in v1.6.0
BuildPayoutRegistrationMessage builds the payout-address ownership message. This MUST be byte-identical to the platform's buildPayoutRegistrationMessage, or registration fails the ownership proof. The org id is in the signed bytes, so a captured signature can never register the address under a different org.
func BuildReplySubject ¶ added in v0.8.0
func ComputePaymentValidityWindow ¶ added in v1.7.0
func ComputePaymentValidityWindow(input ValidityWindowInput) (validAfter, validBefore *big.Int, err error)
ComputePaymentValidityWindow computes the EIP-3009 (validAfter, validBefore) window for a payment, landing inside the band the platform accepts. validBefore governs on-chain validity, so it MUST stay far enough in the future to settle (>= MinHeadroomSec) yet not so far that the total window exceeds the MaxWindowSec cap; validAfter is set generously in the past for clock skew.
Both ends of that band are payer landmines: a too-tight validBefore (low headroom, e.g. a near-expired challenge) is rejected for being about to expire, and a too-wide window (far-future expiry) is rejected as "authorization window too wide". By default this clamps the computed window into the band so a caller who does not override always gets a signable window.
If the caller pins ValidBeforeSec / ValidAfterSec, that is an intent to pin the bound: when it falls outside the band this returns an error naming which bound was violated (rather than signing a doomed authorization), unless Clamp is left enabled, in which case the pinned value is clamped like the computed one.
func ConfirmedHeaders ¶
func DeriveEIP3009Nonce ¶ added in v1.6.0
func DeriveEIP3009Nonce(input NonceBinding) (string, error)
DeriveEIP3009Nonce derives the EIP-3009 nonce bound to a specific interaction step:
keccak256( utf8(lower(interaction_id)) || 0x00
|| utf8(lower(challenge_step_id)) || 0x00
|| hexdecode(challenge_nonce) )
The 0x00 separators pin the field boundaries (undelimited concatenation of variable-length strings is collision-ambiguous), and the challenge nonce is decoded to its 32 raw bytes before hashing. The platform recomputes this and rejects a mismatch. Returns the 0x-prefixed 32-byte hash.
func FormatAddress ¶ added in v0.8.0
func FormatAddress(address ReceivedEmailAddress) string
func IsEmailReceivedEvent ¶
func IsEmailReceivedEventType ¶ added in v1.8.0
IsEmailReceivedEventType reports whether event names the email.received event.
func IsInteractionX402Event ¶ added in v1.8.0
IsInteractionX402Event reports whether event is any interaction.x402.* event.
func IsKnownWebhookEventType ¶ added in v1.8.0
IsKnownWebhookEventType reports whether eventType is a known current catalog value.
func IsPaymentEvent ¶ added in v1.8.0
IsPaymentEvent reports whether event is any payment.* event.
func IsPaymentFailedEvent ¶ added in v1.8.0
IsPaymentFailedEvent reports whether event is the payment.failed event.
func IsPaymentSettledEvent ¶ added in v1.8.0
IsPaymentSettledEvent reports whether event is the payment.settled event.
func IsRawIncluded ¶
func ParseJSONBody ¶
func PrepareStandardWebhooksSecret ¶
PrepareStandardWebhooksSecret strips the "whsec_" prefix if present, then base64-decodes the remainder to produce the raw HMAC key bytes.
func VerifyRawEmailDownload ¶
func VerifyStandardWebhooksSignature ¶
func VerifyStandardWebhooksSignature(options StandardWebhooksVerifyOptions) (bool, error)
VerifyStandardWebhooksSignature verifies a Standard Webhooks signature.
func VerifyWebhookSignature ¶
func VerifyWebhookSignature(options VerifyOptions) (bool, error)
Types ¶
type APIError ¶ added in v0.8.0
type APIError struct {
StatusCode int
Code string
Message string
RetryAfter *int
Gates []primitiveapi.GateDenial
RequestID string
Details *primitiveapi.ErrorResponseErrorDetails
Payload any
}
type AuthConfidence ¶
type AuthConfidence string
const ( AuthConfidenceHigh AuthConfidence = "high" AuthConfidenceMedium AuthConfidence = "medium" AuthConfidenceLow AuthConfidence = "low" )
type AuthVerdict ¶
type AuthVerdict string
const ( AuthVerdictLegit AuthVerdict = "legit" AuthVerdictSuspicious AuthVerdict = "suspicious" AuthVerdictUnknown AuthVerdict = "unknown" )
type BounceAnalysis ¶ added in v1.4.0
type BounceAnalysis struct {
IsBounce bool `json:"is_bounce"`
Kind string `json:"kind"`
Type string `json:"type"`
Category string `json:"category"`
ClassifiedBy string `json:"classified_by"`
FailedRecipient *string `json:"failed_recipient"`
SMTPCode *int64 `json:"smtp_code"`
StatusCode *string `json:"status_code"`
DiagnosticCode *string `json:"diagnostic_code"`
ReportedByMTA *string `json:"reported_by_mta"`
OriginalMessageID *string `json:"original_message_id"`
Reasons []string `json:"reasons"`
}
BounceAnalysis is the parsed delivery status notification carried on email.bounced events.
type BuildPaymentStepEnvelopeInput ¶ added in v1.8.0
type BuildPaymentStepEnvelopeInput struct {
// InteractionID is the thread id (uuid@domain).
InteractionID string
// StepID is a fresh UUID identifying this payment step.
StepID string
// PrevStepID is the challenge step id this payment answers.
PrevStepID string
Payment X402PaymentPayload
// ExpiresAt is an optional ISO-8601 step expiry.
ExpiresAt string
}
BuildPaymentStepEnvelopeInput configures BuildPaymentStepEnvelope.
type BuiltPaymentStep ¶ added in v1.8.0
type BuiltPaymentStep struct {
Envelope InteractionEnvelope
// JSON is the canonical interaction.json body (what to attach to the reply).
JSON string
}
BuiltPaymentStep is a built, signed payment-step envelope plus its canonical JSON bytes. The caller attaches JSON as the interaction.json part of the reply email; the platform reads Envelope back from those exact bytes.
func BuildPaymentStepEnvelope ¶ added in v1.8.0
func BuildPaymentStepEnvelope(input BuildPaymentStepEnvelopeInput) (BuiltPaymentStep, error)
BuildPaymentStepEnvelope builds the section-2.3 interaction.json envelope for a payment step. Pure: no I/O. Payment is the signed exact-EVM payload (from BuildExactEvmPaymentPayload); PrevStepID is the challenge step id this payment answers, and StepID is a fresh UUID for the payment step. Returns the envelope and its canonical JSON, so the bytes the platform reads back are exactly the ones produced here.
type Client ¶ added in v0.8.0
type Client struct {
// contains filtered or unexported fields
}
Client is the high-level Primitive SDK client. Internally it constructs two generated clients (one per host) and routes each operation to the right one so customers don't have to think about the host split:
- api / Host 1 (DefaultAPIBaseURL1, "https://www.primitive.dev/api/v1"): every operation except attachment-capable message sends.
- apiSend / Host 2 (DefaultAPIBaseURL2, "https://api.primitive.dev/v1"): /send-mail and /emails/{id}/reply. Cloudflare Worker with a larger request body cap to support attachment sends and replies.
func NewClient ¶ added in v0.8.0
func NewClient(apiKey string, opts ...primitiveapi.ClientOption) (*Client, error)
func NewClientFromAPI ¶ added in v0.8.0
func NewClientFromAPI(apiClient sendAPI) *Client
NewClientFromAPI wraps a customer-supplied generated client. Useful for tests where the customer wants full control over the underlying HTTP layer. The same client is used for both host-1 and host-2 operations; the caller is responsible for ensuring it points at a host that can serve both shapes (typically only happens in tests against a mock).
func NewClientWithOptions ¶ added in v0.21.0
func NewClientWithOptions(apiKey string, options ClientOptions) (*Client, error)
NewClientWithOptions builds a Client with explicit base-URL overrides. Prefer NewClient unless you are running against staging or local.
func (*Client) Forward ¶ added in v0.8.0
func (c *Client) Forward(ctx context.Context, email *ReceivedEmail, input ForwardParams) (SendResult, error)
func (*Client) Reply ¶ added in v0.8.0
func (c *Client) Reply(ctx context.Context, email *ReceivedEmail, input ReplyParams) (SendResult, error)
Reply sends an outbound reply to an inbound email.
Calls POST /emails/{id}/reply on the server. Recipients, subject, and threading headers are derived server-side from the inbound row identified by email.ID. The customer controls the body, optional From override, optional attachments, and optional Wait flag.
func (*Client) SemanticSearch ¶ added in v0.35.0
func (c *Client) SemanticSearch(ctx context.Context, request *primitiveapi.SemanticSearchInput) (SemanticSearchResponse, error)
SemanticSearch runs a semantic / hybrid / keyword search across received and sent mail (POST /v1/semantic-search on the search host). Returns ranked rows; each row carries matched fields, a match-centered excerpt, and an additive score breakdown. See primitiveapi.SemanticSearchInput for request fields and primitiveapi.SemanticSearchResult for the row shape.
Requires the Pro plan and the semantic_search_enabled entitlement; callers without them receive an APIError with Status: 403.
func (*Client) Send ¶ added in v0.8.0
func (c *Client) Send(ctx context.Context, params SendParams) (SendResult, error)
Send sends an outbound email. The request remains open until Primitive's downstream SMTP transaction completes, so callers should pass a context with a deadline long enough for SMTP delivery, typically 30-60 seconds.
type ClientOptions ¶ added in v0.21.0
type ClientOptions struct {
// APIBaseURL1 overrides the primary API host. Empty = production default.
APIBaseURL1 string
// APIBaseURL2 overrides the attachments-supporting send host. Empty = production default.
APIBaseURL2 string
// Extra options forwarded to both underlying ogen clients (TLS, HTTP
// client, telemetry middleware, etc.).
Extra []primitiveapi.ClientOption
}
ClientOptions configures a NewClient call. Both base URLs default to the production hosts and only need overriding for internal staging/local testing. The overrides are not part of the publicly- documented SDK surface.
type DKIMSignature ¶
type DMARCPolicyPublished ¶ added in v1.4.0
type DMARCRecord ¶ added in v1.4.0
type DMARCReportAnalysis ¶ added in v1.4.0
type DMARCReportAnalysis struct {
Kind string `json:"kind"`
Organization *string `json:"organization"`
ReportID *string `json:"report_id"`
DateRange ReportDateRange `json:"date_range"`
PolicyPublished DMARCPolicyPublished `json:"policy_published"`
TotalCount int64 `json:"total_count"`
DKIMPassCount int64 `json:"dkim_pass_count"`
SPFPassCount int64 `json:"spf_pass_count"`
Records []DMARCRecord `json:"records"`
}
DMARCReportAnalysis is the parsed DMARC aggregate report carried on email.dmarc_report events.
type DkimResult ¶
type DkimResult string
const ( DkimResultPass DkimResult = "pass" DkimResultFail DkimResult = "fail" DkimResultTemperror DkimResult = "temperror" DkimResultPermerror DkimResult = "permerror" )
type DmarcPolicy ¶
type DmarcPolicy string
const ( DmarcPolicyReject DmarcPolicy = "reject" DmarcPolicyQuarantine DmarcPolicy = "quarantine" DmarcPolicyNone DmarcPolicy = "none" )
type DmarcResult ¶
type DmarcResult string
const ( DmarcResultPass DmarcResult = "pass" DmarcResultFail DmarcResult = "fail" DmarcResultNone DmarcResult = "none" DmarcResultTemperror DmarcResult = "temperror" DmarcResultPermerror DmarcResult = "permerror" )
type DownloadInfo ¶
type Email ¶
type Email struct {
ID string `json:"id"`
ReceivedAt string `json:"received_at"`
SMTP SMTPEnvelope `json:"smtp"`
Headers EmailHeaders `json:"headers"`
Content EmailContent `json:"content"`
Parsed ParsedData `json:"parsed"`
Analysis EmailAnalysis `json:"analysis"`
Auth EmailAuth `json:"auth"`
}
type EmailAddress ¶
type EmailAnalysis ¶
type EmailAnalysis struct {
// Spamassassin holds SpamAssassin analysis results.
// Optional. Present when the email was processed by a SpamAssassin-equipped
// pipeline (always present in Primitive's managed service).
Spamassassin *SpamAssassinAnalysis `json:"spamassassin,omitempty"`
// Forward holds forward detection and analysis results.
// Optional. Present when the email was processed by a forward-detection
// pipeline (always present in Primitive's managed service).
Forward *ForwardAnalysis `json:"forward,omitempty"`
// Bounce holds parsed delivery status notification (bounce) details.
// Present on email.bounced events; absent on all other event types.
Bounce *BounceAnalysis `json:"bounce,omitempty"`
// TLSReport holds parsed SMTP TLS report (RFC 8460) details.
// Present on email.tls_report events; absent on all other event types.
TLSReport *TLSReportAnalysis `json:"tls_report,omitempty"`
// DMARCReport holds parsed DMARC aggregate report (RFC 7489) details.
// Present on email.dmarc_report events; absent on all other event types.
DMARCReport *DMARCReportAnalysis `json:"dmarc_report,omitempty"`
}
EmailAnalysis contains email analysis and classification results.
All fields are optional (pointer types). Which fields are present depends on the analysis pipeline processing the email. Primitive's managed service populates all fields. Self-hosted or third-party deployments may include some, all, or none of these fields depending on their pipeline configuration.
A nil field means that particular analysis was not performed, not that analysis produced no results.
type EmailAuth ¶
type EmailAuth struct {
SPF SpfResult `json:"spf"`
DMARC DmarcResult `json:"dmarc"`
DMARCPolicy *DmarcPolicy `json:"dmarcPolicy"`
DMARCFromDomain *string `json:"dmarcFromDomain"`
DMARCSpfAligned *bool `json:"dmarcSpfAligned,omitempty"`
DMARCDkimAligned *bool `json:"dmarcDkimAligned,omitempty"`
DMARCSpfStrict *bool `json:"dmarcSpfStrict"`
DMARCDkimStrict *bool `json:"dmarcDkimStrict"`
DKIMSignatures []DKIMSignature `json:"dkimSignatures"`
}
type EmailContent ¶
type EmailContent struct {
Raw RawContent `json:"raw"`
Download DownloadInfo `json:"download"`
}
type EmailHeaders ¶
type EmailReceivedEvent ¶
type EmailReceivedEvent struct {
ID string `json:"id"`
Event string `json:"event"`
Version string `json:"version"`
Delivery Delivery `json:"delivery"`
Email Email `json:"email"`
}
func HandleWebhook ¶
func HandleWebhook(options HandleWebhookOptions) (*EmailReceivedEvent, error)
func ValidateEmailReceivedEvent ¶
func ValidateEmailReceivedEvent(input any) (*EmailReceivedEvent, error)
func (EmailReceivedEvent) GetEvent ¶
func (e EmailReceivedEvent) GetEvent() string
type ErrorDefinition ¶
type EventType ¶
type EventType string
const ( // EventTypeEmailReceived is a normal inbound email. EventTypeEmailReceived EventType = "email.received" // EventTypeEmailBounced is a delivery status notification (DSN) reporting // that a message delivery failed. Carries Email.Analysis.Bounce. EventTypeEmailBounced EventType = "email.bounced" // EventTypeEmailTLSReport is an SMTP TLS report (RFC 8460). Carries // Email.Analysis.TLSReport. EventTypeEmailTLSReport EventType = "email.tls_report" // EventTypeEmailDMARCReport is a DMARC aggregate report (RFC 7489). Carries // Email.Analysis.DMARCReport. EventTypeEmailDMARCReport EventType = "email.dmarc_report" // EventTypeEmailDMARCFailure is a DMARC failure (forensic) report. EventTypeEmailDMARCFailure EventType = "email.dmarc_failure" )
type ForwardAnalysis ¶
type ForwardAnalysis struct {
Detected bool `json:"detected"`
Results []ForwardResult `json:"results"`
AttachmentsFound int64 `json:"attachments_found"`
AttachmentsAnalyzed int64 `json:"attachments_analyzed"`
AttachmentsLimit *int64 `json:"attachments_limit"`
}
type ForwardOriginalSender ¶
type ForwardParams ¶ added in v0.8.0
type ForwardResult ¶
type ForwardResult struct {
Type string `json:"type"`
AttachmentTarPath *string `json:"attachment_tar_path,omitempty"`
AttachmentFilename *string `json:"attachment_filename,omitempty"`
Analyzed *bool `json:"analyzed,omitempty"`
OriginalSender *ForwardOriginalSender `json:"original_sender"`
Verification *ForwardVerification `json:"verification"`
Summary string `json:"summary"`
}
func (ForwardResult) MarshalJSON ¶
func (r ForwardResult) MarshalJSON() ([]byte, error)
type ForwardVerdict ¶
type ForwardVerdict string
const ( ForwardVerdictLegit ForwardVerdict = "legit" ForwardVerdictUnknown ForwardVerdict = "unknown" )
type ForwardVerification ¶
type ForwardVerification struct {
Verdict ForwardVerdict `json:"verdict"`
Confidence AuthConfidence `json:"confidence"`
DKIMVerified bool `json:"dkim_verified"`
DKIMDomain *string `json:"dkim_domain"`
DMARCPolicy *DmarcPolicy `json:"dmarc_policy"`
}
type HandleWebhookOptions ¶
type InteractionEnvelope ¶ added in v1.8.0
type InteractionEnvelope struct {
InteractionVersion int `json:"interaction_version"`
InteractionID string `json:"interaction_id"`
Protocol string `json:"protocol"`
ProtocolVersion int `json:"protocol_version"`
Step string `json:"step"`
StepID string `json:"step_id"`
// PrevStepID is the id of the step this one answers (the challenge step), or
// null. A pointer so it serializes to JSON null when unset.
PrevStepID *string `json:"prev_step_id"`
// ExpiresAt is an optional ISO-8601 step expiry; null when unset.
ExpiresAt *string `json:"expires_at"`
Payload X402PaymentStepPayload `json:"payload"`
}
InteractionEnvelope is the interaction.json envelope for one step of an email-carried interaction. The payer's payment step is sent as an interaction.json MIME attachment in the reply; the platform parses this envelope, validates the step against the x402.payment protocol, and re-verifies the embedded payment.
type InteractionEvent ¶ added in v1.8.0
InteractionEvent is an interaction.* webhook body.
The stored payload is just {"interaction": {...}} with no event/type field; the parser overlays a canonical Event from the header. Payload preserves the raw stored body verbatim.
func (InteractionEvent) GetEvent ¶ added in v1.8.0
func (e InteractionEvent) GetEvent() string
GetEvent returns the canonical event name (mirrored from the header).
type NonceBinding ¶ added in v1.6.0
type NonceBinding struct {
// InteractionID is the interaction id, including its @domain. Lowercased
// before hashing.
InteractionID string
// ChallengeStepID is the challenge step id (a UUID). Lowercased before
// hashing.
ChallengeStepID string
// ChallengeNonce is the challenger's per-challenge random nonce: 64
// lowercase hex chars.
ChallengeNonce string
}
NonceBinding binds an EIP-3009 nonce to a specific interaction step.
type ParsedData ¶
type ParsedData struct {
Status ParsedStatus `json:"status"`
Error *ParsedError `json:"error"`
BodyText *string `json:"body_text"`
BodyHTML *string `json:"body_html"`
ReplyTo []EmailAddress `json:"reply_to"`
CC []EmailAddress `json:"cc"`
BCC []EmailAddress `json:"bcc"`
ToAddresses []EmailAddress `json:"to_addresses"`
InReplyTo []string `json:"in_reply_to"`
References []string `json:"references"`
Attachments []WebhookAttachment `json:"attachments"`
AttachmentsDownloadURL *string `json:"attachments_download_url"`
}
type ParsedError ¶
type ParsedStatus ¶
type ParsedStatus string
const ( ParsedStatusComplete ParsedStatus = "complete" ParsedStatusFailed ParsedStatus = "failed" )
type PaymentEvent ¶ added in v1.8.0
type PaymentEvent struct {
Event string `json:"event"`
Type string `json:"type,omitempty"`
ChallengeID string `json:"challenge_id,omitempty"`
Network string `json:"network,omitempty"`
Amount string `json:"amount,omitempty"`
Asset string `json:"asset,omitempty"`
PayerOrg *string `json:"payer_org,omitempty"`
SettleTx string `json:"settle_tx,omitempty"`
FailureReason string `json:"failure_reason,omitempty"`
Payload map[string]any `json:"-"`
}
PaymentEvent is a payment.* webhook body.
The stored payload is FLAT (no envelope, no nested payment object): it carries the event name in Type, and the parser overlays a canonical Event mirrored from the header so consumers can branch on a single field. All amounts are token base units (USDC has 6 decimals, so "10000" is 0.01). Payload preserves the raw stored body verbatim. SettleTx is set on payment.settled; FailureReason is set on payment.failed.
func (PaymentEvent) GetEvent ¶ added in v1.8.0
func (e PaymentEvent) GetEvent() string
GetEvent returns the canonical event name (mirrored from the header).
type PrimitiveWebhookError ¶
type PrimitiveWebhookError struct {
NameValue string
CodeValue string
MessageValue string
SuggestionValue string
Cause error
}
func (*PrimitiveWebhookError) As ¶
func (e *PrimitiveWebhookError) As(target any) bool
func (*PrimitiveWebhookError) Code ¶
func (e *PrimitiveWebhookError) Code() string
func (*PrimitiveWebhookError) Error ¶
func (e *PrimitiveWebhookError) Error() string
func (*PrimitiveWebhookError) Message ¶
func (e *PrimitiveWebhookError) Message() string
func (*PrimitiveWebhookError) Name ¶
func (e *PrimitiveWebhookError) Name() string
func (*PrimitiveWebhookError) Suggestion ¶
func (e *PrimitiveWebhookError) Suggestion() string
func (*PrimitiveWebhookError) ToMap ¶
func (e *PrimitiveWebhookError) ToMap() map[string]any
func (*PrimitiveWebhookError) Unwrap ¶
func (e *PrimitiveWebhookError) Unwrap() error
type PrivateKeySigner ¶ added in v1.6.0
type PrivateKeySigner struct {
// contains filtered or unexported fields
}
PrivateKeySigner is an X402Signer backed by an in-memory secp256k1 key.
func NewPrivateKeySigner ¶ added in v1.6.0
func NewPrivateKeySigner(hexKey string) (*PrivateKeySigner, error)
NewPrivateKeySigner builds a signer from a hex-encoded private key (with or without the 0x prefix). The key stays in process memory; it is never sent to the platform.
func (*PrivateKeySigner) Address ¶ added in v1.6.0
func (s *PrivateKeySigner) Address() string
Address returns the signer's checksummed EVM address.
func (*PrivateKeySigner) SignMessage ¶ added in v1.6.0
func (s *PrivateKeySigner) SignMessage(message string) (string, error)
SignMessage signs a UTF-8 string with Ethereum personal_sign (EIP-191): keccak256("\x19Ethereum Signed Message:\n" + len(message) + message), then secp256k1 sign.
func (*PrivateKeySigner) SignTypedData ¶ added in v1.6.0
func (s *PrivateKeySigner) SignTypedData(typedData apitypes.TypedData) (string, error)
SignTypedData signs the EIP-712 digest of the typed data. The returned signature has its recovery id in canonical Ethereum form (v ∈ {27, 28}).
type RawContent ¶
type RawContent struct {
Included bool `json:"included"`
Encoding *string `json:"encoding,omitempty"`
ReasonCode *string `json:"reason_code,omitempty"`
MaxInlineBytes int64 `json:"max_inline_bytes"`
SizeBytes int64 `json:"size_bytes"`
SHA256 string `json:"sha256"`
Data *string `json:"data,omitempty"`
}
type RawEmailDecodeError ¶
type RawEmailDecodeError struct{ PrimitiveWebhookError }
func NewRawEmailDecodeError ¶
func NewRawEmailDecodeError(code string, message string) *RawEmailDecodeError
type ReceiveRequestOptions ¶ added in v0.8.0
type ReceivedEmail ¶ added in v0.8.0
type ReceivedEmail struct {
ID string
EventID string
ReceivedAt string
Sender ReceivedEmailAddress
ReplyTarget ReceivedEmailAddress
ReceivedBy string
ReceivedByAll []string
Subject string
ReplySubject string
ForwardSubject string
Text string
Thread ReceivedEmailThread
Attachments []WebhookAttachment
Auth EmailAuth
Analysis EmailAnalysis
Raw EmailReceivedEvent
}
func NormalizeReceivedEmail ¶ added in v0.8.0
func NormalizeReceivedEmail(event EmailReceivedEvent) (*ReceivedEmail, error)
NormalizeReceivedEmail builds a ReceivedEmail from a validated webhook event. It returns an error rather than panicking when required fields (SMTP recipients) are missing so callers running with hand-built events, replays, or test fixtures get a recoverable failure instead of a process crash.
func Receive ¶ added in v0.8.0
func Receive(options HandleWebhookOptions) (*ReceivedEmail, error)
func ReceiveFromHTTPRequest ¶ added in v0.8.0
func ReceiveFromHTTPRequest(request *http.Request, options ReceiveRequestOptions) (*ReceivedEmail, error)
type ReceivedEmailAddress ¶ added in v0.8.0
func ParseHeaderAddress ¶ added in v0.8.0
func ParseHeaderAddress(value string) *ReceivedEmailAddress
ParseHeaderAddress parses a single RFC 5322 header address (From, Sender, Reply-To). Lenient about quirky headers (unquoted commas in display names, missing closing angle brackets) but strict about the resulting address: the extracted addr-spec must look like a real email or this returns nil and the normalizer falls back to the SMTP envelope sender.
type ReceivedEmailThread ¶ added in v0.8.0
type ReplyParams ¶ added in v0.8.0
type ReplyParams struct {
BodyText string
BodyHTML string
From string
Attachments []SendAttachment
Wait *bool
}
ReplyParams is the input shape for Client.Reply.
Recipients (To), subject ("Re: <parent>"), and threading headers (In-Reply-To, References) are derived server-side from the inbound row referenced by the email's ID. Subject overrides are not supported because Gmail's Conversation View needs both a References match and a normalized-subject match to thread, and a custom subject silently breaks that.
type ReportDateRange ¶ added in v1.4.0
ReportDateRange is the reporting window shared by TLS and DMARC reports.
type SMTPEnvelope ¶
type SemanticSearchResponse ¶ added in v0.35.0
type SemanticSearchResponse struct {
Data []primitiveapi.SemanticSearchResult
Meta primitiveapi.SemanticSearchMeta
}
SemanticSearchResponse is the hand-written return type from Client.SemanticSearch. Mirrors the server's success envelope (data + meta) without the boolean success flag, since errors surface as Go errors instead.
type SendAttachment ¶ added in v0.35.0
type SendAttachment = primitiveapi.SendMailAttachment
type SendParams ¶ added in v0.8.0
type SendResult ¶ added in v0.8.0
type SendResult struct {
ID string
Status primitiveapi.SentEmailStatus
QueueID primitiveapi.NilString
Accepted []string
Rejected []string
ClientIdempotencyKey string
RequestID string
ContentHash string
// IdempotentReplay is true when the response replays a previously
// recorded send keyed by ClientIdempotencyKey (same key, same
// canonical payload). False on a fresh send and on gate-denied
// responses.
IdempotentReplay bool
DeliveryStatus primitiveapi.OptDeliveryStatus
SMTPResponseCode primitiveapi.OptNilInt
SMTPResponseText primitiveapi.OptString
}
type SendThread ¶ added in v0.8.0
type SignInteractionPaymentInput ¶ added in v1.7.0
type SignInteractionPaymentInput struct {
// Sign signs the EIP-712 typed data with the caller's own key and returns a
// 0x hex signature (e.g. PrivateKeySigner.SignTypedData).
Sign func(typedData apitypes.TypedData) (string, error)
// Payer is the from address.
Payer string
Domain TokenDomain
// PayTo is the recipient (the challenger's payTo).
PayTo string
// Amount is in token base units.
Amount *big.Int
NonceBinding NonceBinding
ValidAfter *big.Int
ValidBefore *big.Int
}
SignInteractionPaymentInput configures SignInteractionPayment.
type SignResult ¶
type SignResult struct {
Header string `json:"header"`
Timestamp int64 `json:"timestamp"`
V1 string `json:"v1"`
}
func SignWebhookPayload ¶
func SignWebhookPayload(rawBody any, secret any, timestamps ...int64) (SignResult, error)
type SpamAssassinAnalysis ¶
type SpamAssassinAnalysis struct {
Score float64 `json:"score"`
}
type StandardWebhooksSignResult ¶
type StandardWebhooksSignResult struct {
Signature string `json:"signature"`
MsgID string `json:"msg_id"`
Timestamp int64 `json:"timestamp"`
}
func SignStandardWebhooksPayload ¶
func SignStandardWebhooksPayload(rawBody any, secret any, msgID string, timestamps ...int64) (StandardWebhooksSignResult, error)
SignStandardWebhooksPayload signs a payload using the Standard Webhooks format.
type TLSReportAnalysis ¶ added in v1.4.0
type TLSReportAnalysis struct {
Kind string `json:"kind"`
Organization *string `json:"organization"`
ReportID *string `json:"report_id"`
Contact *string `json:"contact"`
DateRange ReportDateRange `json:"date_range"`
TotalSuccessfulSessions int64 `json:"total_successful_sessions"`
TotalFailedSessions int64 `json:"total_failed_sessions"`
Policies []TLSReportPolicy `json:"policies"`
}
TLSReportAnalysis is the parsed SMTP TLS report carried on email.tls_report events.
type TLSReportFailure ¶ added in v1.4.0
type TLSReportPolicy ¶ added in v1.4.0
type TLSReportPolicy struct {
PolicyDomain *string `json:"policy_domain"`
PolicyType *string `json:"policy_type"`
SuccessfulSessions int64 `json:"successful_sessions"`
FailedSessions int64 `json:"failed_sessions"`
Failures []TLSReportFailure `json:"failures"`
}
type TokenDomain ¶ added in v1.6.0
TokenDomain is the token's EIP-712 domain. name/version MUST be the actual token's domain params; they come from the challenge's payment requirements extra. A wrong name/version produces a signature the verifier rejects.
type TransferAuthorization ¶ added in v1.6.0
type TransferAuthorization struct {
From string
To string
Value *big.Int
ValidAfter *big.Int
ValidBefore *big.Int
// Nonce is the 0x-prefixed 32-byte interaction-bound nonce.
Nonce string
}
TransferAuthorization is the EIP-3009 TransferWithAuthorization message.
func SignInteractionPayment ¶ added in v1.7.0
func SignInteractionPayment(input SignInteractionPaymentInput) (TransferAuthorization, string, error)
SignInteractionPayment derives the bound nonce, assembles the authorization, and signs it. This is the one piece a stock x402 signer cannot do (it generates the nonce internally with no injection point). The key never leaves the caller.
type TrustReason ¶ added in v1.19.0
type TrustReason string
TrustReason is a stable machine-readable code explaining why an email was or was not trusted by IsTrustedSender.
const ( // TrustReasonTrusted means every check passed. TrustReasonTrusted TrustReason = "trusted" // TrustReasonAuthMissing means the event carried no usable auth // object. Present for cross-SDK parity of the reason vocabulary; // the typed EmailReceivedEvent input makes it unreachable in Go. TrustReasonAuthMissing TrustReason = "auth-missing" // TrustReasonAuthSuspicious means ValidateEmailAuth returned a // suspicious verdict (DMARC/SPF failure signals). TrustReasonAuthSuspicious TrustReason = "auth-suspicious" // TrustReasonDmarcTemperror means DMARC evaluation hit a temporary // DNS error. The only retryable reason; the same email may verify // cleanly once DNS recovers. TrustReasonDmarcTemperror TrustReason = "dmarc-temperror" // TrustReasonAuthUnknown means authenticity could not be determined // and the cause is not transient (most commonly the sender domain // publishes no DMARC record, or evaluation hit a permanent error). TrustReasonAuthUnknown TrustReason = "auth-unknown" // TrustReasonDmarcDomainMismatch means the email authenticated, but // the domain DMARC evaluated (the RFC 5322 From domain seen by the // server) is not the expected domain. TrustReasonDmarcDomainMismatch TrustReason = "dmarc-domain-mismatch" // TrustReasonFromHeaderMultipleAddresses means the From header lists // more than one address, which is ambiguous as an identity. TrustReasonFromHeaderMultipleAddresses TrustReason = "from-header-multiple-addresses" // TrustReasonFromHeaderInvalid means the From header is missing, // malformed, or fails address validation. TrustReasonFromHeaderInvalid TrustReason = "from-header-invalid" // TrustReasonFromDomainMismatch means the parsed From address's // domain is not the expected domain. TrustReasonFromDomainMismatch TrustReason = "from-domain-mismatch" // TrustReasonSenderMismatch means Sender was given in the options // and the parsed From address is a different address. TrustReasonSenderMismatch TrustReason = "sender-mismatch" )
type TrustedSenderOptions ¶ added in v1.19.0
type TrustedSenderOptions struct {
// Domain is the domain the email must be authenticated as (the
// RFC 5322 From domain). Matched exactly, case-insensitively: mail
// from a subdomain of Domain does not match. Pass the subdomain
// itself to accept it. Required.
Domain string
// Sender optionally requires an exact sender, as a bare address
// (user@example.com). Compared case-insensitively against the
// parsed From address. Note that DMARC authenticates the domain,
// not the local part: the domain owner's infrastructure controls
// which local parts it signs mail for. Empty means not checked.
Sender string
}
TrustedSenderOptions configures IsTrustedSender.
type TrustedSenderResult ¶ added in v1.19.0
type TrustedSenderResult struct {
// Trusted is true when the email is authenticated as the expected
// domain (and sender, if given).
Trusted bool
// Retryable is true only for transient failures (dmarc-temperror).
// Callers that respond with a 5xx let webhook redelivery retry the
// same email after DNS recovers. All other untrusted reasons are
// permanent for this email.
Retryable bool
// Reason is the code for the first check that failed, or trusted.
Reason TrustReason
// Auth is the underlying ValidateEmailAuth result, for logging.
Auth ValidateEmailAuthResult
}
TrustedSenderResult is the trust decision for an inbound email.
func IsTrustedSender ¶ added in v1.19.0
func IsTrustedSender(event EmailReceivedEvent, opts TrustedSenderOptions) (TrustedSenderResult, error)
IsTrustedSender checks whether an inbound email is authenticated as an expected domain (and optionally an exact sender address).
Trusted is true only when ALL of the following hold:
- ValidateEmailAuth(event.Email.Auth) returns a legit verdict.
- event.Email.Auth.DMARCFromDomain (the domain the server's DMARC evaluation ran against) equals opts.Domain.
- The From header strict-parses to exactly one valid address whose domain equals opts.Domain.
- When opts.Sender is non-empty, the parsed From address equals it exactly (case-insensitive).
The verdict alone says an email was authenticated, not which domain it was authenticated as: a fully authenticated email from an attacker-controlled domain is legit. Anchoring DMARCFromDomain closes that. The strict From parse defends the remaining gaps: a header like `From: "trusted@example.com" <x@evil.com>` plants an allowlisted address in the display name while DMARC evaluates evil.com, and NormalizeReceivedEmail's Sender is not a safe anchor because its lenient parser falls back to the attacker-controlled SMTP envelope sender. Reply-To is likewise never consulted.
An unknown verdict caused by a DMARC temperror is surfaced with Retryable true (respond 5xx and let webhook redelivery retry); every other unknown, such as a sender domain with no DMARC record, is permanent for this email and not retryable.
Malformed event content yields an untrusted result with a reason; an error is returned only for invalid options.
type UnknownEvent ¶
type UnknownEvent struct {
Event string `json:"event"`
ID *string `json:"id,omitempty"`
Version *string `json:"version,omitempty"`
Payload map[string]any `json:"-"`
}
func (UnknownEvent) GetEvent ¶
func (e UnknownEvent) GetEvent() string
func (UnknownEvent) MarshalJSON ¶
func (e UnknownEvent) MarshalJSON() ([]byte, error)
func (*UnknownEvent) UnmarshalJSON ¶
func (e *UnknownEvent) UnmarshalJSON(data []byte) error
type ValidateEmailAuthResult ¶
type ValidateEmailAuthResult struct {
Verdict AuthVerdict `json:"verdict"`
Confidence AuthConfidence `json:"confidence"`
Reasons []string `json:"reasons"`
}
func ValidateEmailAuth ¶
func ValidateEmailAuth(input any) (ValidateEmailAuthResult, error)
ValidateEmailAuth computes an authentication verdict from SPF, DKIM, and DMARC results.
A legit verdict means the email authenticated as its own From domain, not as any particular domain you trust: a fully authenticated email from any domain returns legit. For authorization decisions, anchor the verdict to an expected domain with IsTrustedSender instead of checking the verdict alone.
type ValidationIssue ¶
type ValidationResult ¶
type ValidationResult[T any] struct { Success bool Data T Error *WebhookValidationError }
func SafeValidateEmailReceivedEvent ¶
func SafeValidateEmailReceivedEvent(input any) ValidationResult[*EmailReceivedEvent]
type ValidityWindowInput ¶ added in v1.7.0
type ValidityWindowInput struct {
ChallengeExpiresAtSec int64
NowSec int64
SettlementMarginSec int64
ClockSkewSec int64
MaxWindowSec int64
// MinHeadroomSec is the minimum validBefore - NowSec. Defaults to
// DefaultMinSettlementHeadroomSec when zero.
MinHeadroomSec int64
// ValidBeforeSec / ValidAfterSec pin validBefore / validAfter (unix seconds).
// Pointers so a zero value is distinguishable from unset; when nil the value
// is derived (expiry + margin / now - skew).
ValidBeforeSec *int64
ValidAfterSec *int64
// Clamp, when nil or true, lands an out-of-band window inside the accepted
// band instead of erroring. Set it to a pointer to false to reject a
// caller-pinned override that is out of band with a specific error.
Clamp *bool
}
ValidityWindowInput configures ComputePaymentValidityWindow. The optional fields default when left zero: SettlementMarginSec and ClockSkewSec to 5 minutes, MaxWindowSec to DefaultMaxWindowSec, MinHeadroomSec to DefaultMinSettlementHeadroomSec.
type VerifyOptions ¶
type WebhookAttachment ¶
type WebhookEvent ¶
type WebhookEvent interface {
GetEvent() string
}
func HandleWebhookEvent ¶
func HandleWebhookEvent(options HandleWebhookOptions) (WebhookEvent, error)
func ParseWebhookEvent ¶
func ParseWebhookEvent(input any, eventType ...string) (WebhookEvent, error)
ParseWebhookEvent classifies a webhook payload into a typed event.
The event name is carried in the X-Webhook-Event HEADER for every event family; pass it as the optional eventType argument (HandleWebhookEvent reads it for you). The stored body is sent verbatim with no envelope: email.* bodies carry "event", payment.* bodies carry the name in "type", and interaction.* bodies are just {"interaction": {...}} with no event/type field. The header is therefore the PRIMARY discriminator; a top-level "event" string in the body is used only as a backward-compat fallback.
type WebhookPayloadError ¶
type WebhookPayloadError struct{ PrimitiveWebhookError }
func NewWebhookPayloadError ¶
func NewWebhookPayloadError(code string, message string, suggestion string, cause error) *WebhookPayloadError
type WebhookValidationError ¶
type WebhookValidationError struct {
PrimitiveWebhookError
Field string
ValidationErrors []ValidationIssue
AdditionalErrorCount int
}
func NewWebhookValidationError ¶
func NewWebhookValidationError(field string, message string, suggestion string, validationErrors []ValidationIssue) *WebhookValidationError
func (*WebhookValidationError) ToMap ¶
func (e *WebhookValidationError) ToMap() map[string]any
type WebhookVerificationError ¶
type WebhookVerificationError struct{ PrimitiveWebhookError }
func NewWebhookVerificationError ¶
func NewWebhookVerificationError(code string, message string, suggestion string) *WebhookVerificationError
type X402Challenge ¶ added in v1.6.0
type X402Challenge struct {
ID string `json:"id"`
Network string `json:"network"`
Amount string `json:"amount"`
PayTo string `json:"pay_to"`
NonceBinding X402NonceBinding `json:"nonce_binding"`
PaymentRequirements X402PaymentRequirements `json:"payment_requirements"`
ExpiresAt string `json:"expires_at"`
}
X402Challenge is a request for payment, as returned by Charge / the platform.
type X402ChargeInput ¶ added in v1.6.0
type X402ChargeInput struct {
// Amount in token base units (USDC has 6 decimals, so "10000" = 0.01).
// Provide exactly one of Amount or AmountUsdc.
Amount string
// AmountUsdc is the amount as human USDC (e.g. "0.01"), converted to base
// units for you. Provide exactly one of Amount or AmountUsdc.
AmountUsdc string
// Network defaults to "base-sepolia".
Network string
// PayerOrg is the org id allowed to pay this challenge (on-net binding).
PayerOrg string
Description string
// Resource is a URL identifying the thing being paid for.
Resource string
// ExpiresIn is seconds until the challenge expires (default 1h server-side).
// A pointer so 0 ("never"/server-default) is distinguishable from unset.
ExpiresIn *int
// IdempotencyKey, when set, is sent as the Idempotency-Key HTTP header on the
// create-challenge request. Retrying Charge with the same key returns the
// original challenge instead of creating a duplicate. Mirrors the Node SDK.
IdempotencyKey string
}
X402ChargeInput is the input shape for X402Client.Charge.
type X402Client ¶ added in v1.6.0
type X402Client struct {
// contains filtered or unexported fields
}
X402Client is a non-custodial client for x402 agent-to-agent payments. Charge (payee) asks for a payment; Pay (payer) signs and settles it with the customer's own key. Mirrors the Node SDK's X402Client.
func NewX402Client ¶ added in v1.6.0
func NewX402Client(options X402ClientOptions) *X402Client
NewX402Client builds an X402Client. With zero options it reads PRIMITIVE_API_KEY from the environment and targets the production host.
func (*X402Client) Charge ¶ added in v1.6.0
func (c *X402Client) Charge(ctx context.Context, input X402ChargeInput) (*X402Challenge, error)
Charge requests a payment (payee side). Returns the challenge to hand to the payer. POST /v1/x402/challenges.
func (*X402Client) CreateEmailChallenge ¶ added in v1.8.0
func (c *X402Client) CreateEmailChallenge(ctx context.Context, input X402EmailChargeInput) (*X402EmailChallenge, error)
CreateEmailChallenge issues a payment challenge over an email thread (payee side). Sends the challenge as an email from From to To and binds the payment to that thread. Returns the challenge (including the real InteractionID); deliver it to the payer, who calls PayEmailChallenge to build the signed payment step. Provide exactly one of Amount (base units) or AmountUsdc (human USDC). POST /v1/x402/email-challenges.
func (*X402Client) GetChallenge ¶ added in v1.6.0
func (c *X402Client) GetChallenge(ctx context.Context, id string) (*X402Challenge, error)
GetChallenge fetches a challenge by id (scoped to the challenger org that created it). GET /v1/x402/challenges/{id}.
func (*X402Client) GetSpendPolicy ¶ added in v1.6.0
func (c *X402Client) GetSpendPolicy(ctx context.Context) (*X402SpendPolicy, error)
GetSpendPolicy reads your org's spend policy (kill-switch + caps + allowlist). GET /v1/x402/spend-policy.
func (*X402Client) ListDeclinedPayments ¶ added in v1.6.0
func (c *X402Client) ListDeclinedPayments(ctx context.Context) ([]X402DeclinedPayment, error)
ListDeclinedPayments lists the most recent payments your org's spend policy refused (newest first). Use it to see why an outbound payment was declined. GET /v1/x402/declined-payments.
func (*X402Client) ListPayoutAddresses ¶ added in v1.6.0
func (c *X402Client) ListPayoutAddresses(ctx context.Context) ([]X402PayoutAddress, error)
ListPayoutAddresses lists your org's registered payout addresses. GET /v1/x402/payout-addresses.
func (*X402Client) Pay ¶ added in v1.6.0
func (c *X402Client) Pay(ctx context.Context, challenge *X402Challenge, signer X402Signer) (*X402Receipt, error)
Pay pays a challenge (payer side). Derives the interaction-bound authorization, signs it locally with the caller's key, and submits it for settlement. POST /v1/x402/challenges/{id}/pay.
func (*X402Client) PayEmailChallenge ¶ added in v1.8.0
func (c *X402Client) PayEmailChallenge(challenge *X402EmailChallenge, signer X402Signer) (*BuiltPaymentStep, error)
PayEmailChallenge builds the signed payment step for an email-native challenge (payer side). Given a received X402EmailChallenge and the caller's signer, it derives the interaction-bound authorization, signs it locally, and returns the signed interaction.json payment-step envelope plus its canonical JSON bytes. It does NOT send anything.
The caller sends BuiltPaymentStep.JSON back as an interaction.json attachment on a reply to the challenge email; the platform reads the envelope from those exact bytes, re-derives the bound nonce, and settles.
func (*X402Client) RegisterPayoutAddress ¶ added in v1.6.0
func (c *X402Client) RegisterPayoutAddress(ctx context.Context, input X402PayoutRegistrationInput, signer X402Signer) (*X402PayoutAddress, error)
RegisterPayoutAddress registers a payout address for your org (payee side). The signer proves control of its own address with an org-bound personal_sign; the proven address becomes (or updates to) the default payout destination for the network. POST /v1/x402/payout-addresses.
Org is optional: when input.Org is empty it is resolved from your authenticated account (GET /v1/account), so most callers never need to supply it.
func (*X402Client) SetSpendPolicy ¶ added in v1.6.0
func (c *X402Client) SetSpendPolicy(ctx context.Context, update X402SpendPolicyUpdate) (*X402SpendPolicy, error)
SetSpendPolicy updates your org's spend policy. The endpoint is a PUT, but the server applies it as a merge: only the fields you set are changed and omitted fields keep their current value, so a partial update can't silently reset the kill-switch. Use ClearMaxPerPayment / ClearMaxPerDay to remove a cap. PUT /v1/x402/spend-policy.
type X402ClientOptions ¶ added in v1.6.0
type X402ClientOptions struct {
// APIKey defaults to the PRIMITIVE_API_KEY environment variable.
APIKey string
// BaseURL defaults to the production host (DefaultX402BaseURL).
BaseURL string
// HTTPClient overrides the http.Client (e.g. for testing). Defaults to a
// client with Timeout set from TimeoutMs.
HTTPClient *http.Client
// TimeoutMs is the per-request timeout in milliseconds. Defaults to 30000.
// Ignored when HTTPClient is supplied.
TimeoutMs int
}
X402ClientOptions configures a NewX402Client call.
type X402DeclinedPayment ¶ added in v1.6.0
type X402DeclinedPayment struct {
ID string `json:"id"`
ChallengeID *string `json:"challenge_id"`
CounterpartyOrg *string `json:"counterparty_org"`
Network string `json:"network"`
Amount string `json:"amount"`
Reason string `json:"reason"`
DeclinedAt string `json:"declined_at"`
}
X402DeclinedPayment is a payment the org's spend policy refused (read shape). Mirrors the Node SDK's X402DeclinedPayment.
type X402EmailChallenge ¶ added in v1.8.0
type X402EmailChallenge struct {
InteractionID string `json:"interaction_id"`
ChallengeID string `json:"challenge_id"`
Challenge X402EmailChallengeDetails `json:"challenge"`
}
X402EmailChallenge is the result of issuing an email-native payment challenge. InteractionID is the real email thread id (uuid@domain) the payment is bound to. Hand the whole object to the payer, who calls PayEmailChallenge with it to build the signed payment step.
func ExtractEmailChallenge ¶ added in v1.11.0
func ExtractEmailChallenge(part []byte) (*X402EmailChallenge, error)
ExtractEmailChallenge parses the bytes of an inbound interaction.json MIME part into a typed *X402EmailChallenge ready for PayEmailChallenge.
A payer receives the x402 challenge as an interaction.json attachment on an inbound email (filename interaction.json, content type application/json). This validates the envelope (the strict snake_case wire shape, that it is the x402.payment challenge step, and that the embedded payload carries the fields a payer signs over) and re-assembles the nonce binding from the envelope's interaction_id + step_id + the payload's challenge_nonce, so the caller never has to hand-parse the part. Returns an *X402Error (Status 0) on any malformed or non-challenge part.
The resulting ChallengeID is empty: the platform's private challenge id is not carried on the wire, and PayEmailChallenge does not need it (it binds to the InteractionID and the challenge step id).
type X402EmailChallengeDetails ¶ added in v1.8.0
type X402EmailChallengeDetails struct {
PaymentRequirements X402PaymentRequirements `json:"payment_requirements"`
NonceBinding X402NonceBinding `json:"nonce_binding"`
ExpiresAt string `json:"expires_at"`
}
X402EmailChallengeDetails is the challenge the payer signs and pays, carried inside an email-native challenge response.
type X402EmailChargeInput ¶ added in v1.8.0
type X402EmailChargeInput struct {
// From is your sending address (the payee / funds receiver).
From string
// To is the payer's email address the challenge is sent to.
To string
// Amount in token base units (USDC has 6 decimals, so "10000" = 0.01).
// Provide exactly one of Amount or AmountUsdc.
Amount string
// AmountUsdc is the amount as human USDC (e.g. "0.01"), converted to base
// units for you. Provide exactly one of Amount or AmountUsdc.
AmountUsdc string
// Network defaults to "base-sepolia".
Network string
Description string
// Resource is a URL identifying the thing being paid for.
Resource string
// ExpiresIn is seconds until the challenge expires (defaults to 300s /
// 5 minutes server-side). A pointer so 0 is distinguishable from unset.
ExpiresIn *int
// IdempotencyKey, when set, is sent as the Idempotency-Key HTTP header.
// Retrying with the same key returns the original challenge without sending a
// second email.
IdempotencyKey string
}
X402EmailChargeInput is the input shape for X402Client.CreateEmailChallenge.
type X402Error ¶ added in v1.6.0
type X402Error struct {
Message string
// Status is the HTTP status, or 0 for a client-side / transport error that
// never reached the server.
Status int
// Body is the parsed error envelope or raw text, when available.
Body any
// RetryAfter is the Retry-After response header, if the server sent one.
RetryAfter string
// Cause is the wrapped transport error, if any.
Cause error
}
X402Error is returned by every X402Client method on a client-side, transport, or non-2xx server error. Mirrors the Node SDK's X402Error.
type X402NonceBinding ¶ added in v1.6.0
type X402NonceBinding struct {
InteractionID string `json:"interaction_id"`
ChallengeStepID string `json:"challenge_step_id"`
ChallengeNonce string `json:"challenge_nonce"`
}
X402NonceBinding is the server-supplied nonce binding for a challenge.
type X402PaymentPayload ¶ added in v1.6.0
type X402PaymentPayload struct {
X402Version int `json:"x402Version"`
Scheme string `json:"scheme"`
Network string `json:"network"`
Payload struct {
Signature string `json:"signature"`
Authorization struct {
From string `json:"from"`
To string `json:"to"`
Value string `json:"value"`
ValidAfter string `json:"validAfter"`
ValidBefore string `json:"validBefore"`
Nonce string `json:"nonce"`
} `json:"authorization"`
} `json:"payload"`
}
X402PaymentPayload is the x402 wire payload (validated server-side against the x402 schema).
func BuildExactEvmPaymentPayload ¶ added in v1.7.0
func BuildExactEvmPaymentPayload(network string, auth TransferAuthorization, signature string) (X402PaymentPayload, error)
BuildExactEvmPaymentPayload assembles (and validates) the exact-EVM x402 wire payload. The numeric authorization fields are decimal strings in the wire schema, so the big.Ints are stringified; the nonce passes through as hex. Validation rejects a malformed nonce or signature loudly rather than emitting a payload the platform will reject.
type X402PaymentRequirements ¶ added in v1.6.0
type X402PaymentRequirements struct {
Scheme string `json:"scheme"`
Network string `json:"network"`
MaxAmountRequired string `json:"maxAmountRequired"`
PayTo string `json:"payTo"`
Asset string `json:"asset"`
Extra struct {
Name string `json:"name"`
Version string `json:"version"`
} `json:"extra"`
}
X402PaymentRequirements is the x402 PaymentRequirements the payer signs over.
type X402PaymentStepPayload ¶ added in v1.8.0
type X402PaymentStepPayload struct {
Payment X402PaymentPayload `json:"payment"`
}
X402PaymentStepPayload is the payload of an x402.payment payment step: the signed x402 payload.
type X402PayoutAddress ¶ added in v1.6.0
type X402PayoutAddress struct {
ID string `json:"id"`
Address string `json:"address"`
Network string `json:"network"`
Label *string `json:"label"`
IsDefault bool `json:"is_default"`
VerifiedAt *string `json:"verified_at"`
}
X402PayoutAddress is a registered payout address (read shape).
type X402PayoutRegistrationInput ¶ added in v1.6.0
type X402PayoutRegistrationInput struct {
// Org is the org id the address is being authorized for. Optional: when
// empty it is resolved from your authenticated account (GET /v1/account), so
// most callers never need to supply it.
Org string
// Network defaults to "base-sepolia".
Network string
// IssuedAt is an ISO-8601 timestamp; defaults to time.Now() in UTC.
IssuedAt string
// Label is an optional human label. A pointer so "" can be sent explicitly.
Label *string
}
X402PayoutRegistrationInput is the input shape for X402Client.RegisterPayoutAddress.
type X402Receipt ¶ added in v1.6.0
type X402Receipt struct {
ID string `json:"id"`
Status string `json:"status"`
SettleTx *string `json:"settle_tx"`
}
X402Receipt is the result of paying a challenge.
type X402Signer ¶ added in v1.6.0
type X402Signer interface {
// Address returns the signer's 0x-prefixed checksummed EVM address.
Address() string
// SignTypedData signs an EIP-712 typed-data structure and returns the
// 0x-prefixed 65-byte signature.
SignTypedData(typedData apitypes.TypedData) (string, error)
// SignMessage signs a UTF-8 string via Ethereum personal_sign (EIP-191).
// Only needed for RegisterPayoutAddress (the ownership proof).
SignMessage(message string) (string, error)
}
X402Signer is a customer-held signer. The key never leaves the caller. A PrivateKeySigner (built from a hex private key) satisfies this directly; any key source (hardware wallet, remote KMS) can be adapted by implementing the interface.
type X402SpendPolicy ¶ added in v1.6.0
type X402SpendPolicy struct {
// Paused is a kill-switch: when true, all outbound payments are refused.
Paused bool `json:"paused"`
// MaxPerPayment is the per-payment cap in token base units, or nil for no cap.
MaxPerPayment *string `json:"max_per_payment"`
// MaxPerDay is the daily cap in token base units, or nil for no cap.
MaxPerDay *string `json:"max_per_day"`
// Allowlist is the allowed payee org ids; nil = any on-net payee,
// [] = deny all.
Allowlist []string `json:"allowlist"`
}
X402SpendPolicy is the org's spend policy (read shape; also accepted by SetSpendPolicy as a partial update via X402SpendPolicyUpdate).
type X402SpendPolicyUpdate ¶ added in v1.6.0
type X402SpendPolicyUpdate struct {
Paused *bool
MaxPerPayment *string
MaxPerDay *string
Allowlist *[]string
// contains filtered or unexported fields
}
X402SpendPolicyUpdate is a partial spend-policy update for X402Client.SetSpendPolicy. Only non-nil fields are sent; the server merges, so omitted fields keep their current value. Set a *cap pointer to a pointer to nil... use the helper constructors or build the JSON-ready map yourself.
func (*X402SpendPolicyUpdate) ClearMaxPerDay ¶ added in v1.6.0
func (u *X402SpendPolicyUpdate) ClearMaxPerDay() *X402SpendPolicyUpdate
ClearMaxPerDay sends null for the daily cap (removes the cap).
func (*X402SpendPolicyUpdate) ClearMaxPerPayment ¶ added in v1.6.0
func (u *X402SpendPolicyUpdate) ClearMaxPerPayment() *X402SpendPolicyUpdate
ClearMaxPerPayment sends null for the per-payment cap (removes the cap).
func (*X402SpendPolicyUpdate) SetAllowlist ¶ added in v1.6.0
func (u *X402SpendPolicyUpdate) SetAllowlist(v []string) *X402SpendPolicyUpdate
SetAllowlist sets the payee allowlist on the update.
func (*X402SpendPolicyUpdate) SetMaxPerDay ¶ added in v1.6.0
func (u *X402SpendPolicyUpdate) SetMaxPerDay(v string) *X402SpendPolicyUpdate
SetMaxPerDay sets the daily cap on the update.
func (*X402SpendPolicyUpdate) SetMaxPerPayment ¶ added in v1.6.0
func (u *X402SpendPolicyUpdate) SetMaxPerPayment(v string) *X402SpendPolicyUpdate
SetMaxPerPayment sets the per-payment cap on the update.
func (*X402SpendPolicyUpdate) SetPaused ¶ added in v1.6.0
func (u *X402SpendPolicyUpdate) SetPaused(v bool) *X402SpendPolicyUpdate
SetPaused sets the kill-switch field on the update.