primitive

package module
v1.22.0 Latest Latest
Warning

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

Go to latest
Published: Jul 16, 2026 License: MIT Imports: 30 Imported by: 0

README

github.com/primitivedotdev/sdks/sdk-go

Official Primitive Go SDK.

The package is intentionally centered on a small inbound/outbound email automation flow:

  • primitive.Receive(...)
  • primitive.NewClient(...)
  • client.Send(...)
  • client.Reply(...)
  • client.Forward(...)

The generated HTTP API and lower-level webhook helpers remain available for advanced use.

Requirements

  • Go >=1.25

Installation

go get github.com/primitivedotdev/sdks/sdk-go@latest

Basic usage

Receive and reply
package main

import (
	"context"
	"log"
	"time"

	primitive "github.com/primitivedotdev/sdks/sdk-go"
)

func handle(ctx context.Context, body []byte, headers map[string]string) {
	email, err := primitive.Receive(primitive.HandleWebhookOptions{
		Body:    body,
		Headers: headers,
		Secret:  "whsec_...",
	})
	if err != nil {
		log.Printf("invalid webhook: %v", err)
		return
	}

	client, err := primitive.NewClient("prim_test")
	if err != nil {
		log.Fatal(err)
	}

	_, err = client.Reply(ctx, email, primitive.ReplyParams{BodyText: "Thank you for your email."})
	if err != nil {
		log.Printf("reply failed: %v", err)
	}
}
Send a new email
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
wait := true

result, err := client.Send(ctx, primitive.SendParams{
	From:    "Support <support@example.com>",
	To:      "alice@example.com",
	Subject: "Hello",
	BodyText: "Hi there",
	// Use a unique key per logical send. Reusing a key returns the original
	// response from the first send, which is how retries are deduplicated.
	IdempotencyKey: "customer-key-abc123",
	Wait:           &wait,
	WaitTimeoutMs:  5000,
})

Send, Reply, and Forward keep the HTTP request open until Primitive's downstream SMTP transaction completes. Use a context deadline long enough for SMTP delivery, typically 30-60 seconds.

Per-call timeout and cancellation

Every client method takes ctx context.Context as its first argument, so per-call deadlines, cancellation, and request-scoped values use the standard library directly. There is no separate RequestOptions struct.

// Per-call timeout: cancel after 15 seconds.
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
_, err := client.Send(ctx, primitive.SendParams{
    From:    "Support <support@example.com>",
    To:      "alice@example.com",
    Subject: "Hello",
    BodyText: "Hi there",
})

// Per-call cancellation: bail out from another goroutine.
ctx, cancel := context.WithCancel(context.Background())
go func() { <-userBailoutSignal; cancel() }()
_, err := client.Send(ctx, primitive.SendParams{ /* params */ })

A canceled ctx surfaces as context.Canceled; a deadline exceeded surfaces as context.DeadlineExceeded. Both are distinct from API errors returned as *primitive.APIError, so callers can tell a client-side abort apart from a server response.

For idempotent retries, set IdempotencyKey on SendParams or ForwardParams (see the Send example above). The same key replays the original response.

About Wait mode

When Wait is true, the call returns the first downstream SMTP outcome (or WaitTimeoutMs, default 30000). Possible terminal DeliveryStatus values:

  • delivered accepted by the receiving MTA
  • bounced rejected by the receiving MTA (the response is still 200 OK)
  • deferred temporary failure, the receiving MTA may retry
  • wait_timeout no outcome was observed in time. Treat as "outcome unknown." The send may still complete after the response returns.
Reply from a different address

Reply defaults the From address to the inbound recipient (the address that received the email). When your verified outbound domain differs from your inbound domain, pass From explicitly:

_, err = client.Reply(ctx, email, primitive.ReplyParams{
	BodyText: "Thanks for your email.",
	From:     "notifications@outbound.example.com",
})
HTML replies and waiting on the delivery outcome

Reply accepts BodyHTML as a sibling of BodyText, plus the same Wait flag the top-level Send takes:

wait := true
_, err = client.Reply(ctx, email, primitive.ReplyParams{
	BodyText: "Thanks for your email.",
	BodyHTML: "<p>Thanks for your email.</p>",
	Attachments: []primitive.SendAttachment{
		{
			Filename:      "report.txt",
			ContentBase64: "aGVsbG8=",
		},
	},
	Wait:     &wait,
})

A subject override is intentionally not exposed on ReplyParams. Gmail's Conversation View needs both a References match and a normalized-subject match to thread, so a custom subject silently breaks the thread for half the recipient population. Use client.Send(...) if you need full subject control.

If the inbound row is not in a state we can reply to (no Message-Id recorded, or content was discarded), the API returns inbound_not_repliable (HTTP 422) and the SDK returns an error.

Forward an inbound email
_, err = client.Forward(context.Background(), email, primitive.ForwardParams{
	To:       "ops@example.com",
	BodyText: "Can you take this one?",
})

The normalized email object

primitive.Receive(...) returns a normalized inbound email object with fields such as:

email.Sender.Address
email.ReceivedBy
email.ReplyTarget.Address
email.ReplySubject
email.ForwardSubject
email.Subject
email.Text
email.Thread.MessageID
email.Thread.References
email.Raw

Deciding whether to trust an inbound email

Every email.received event carries the server's SPF, DKIM, and DMARC results on event.Email.Auth. ValidateEmailAuth computes an overall verdict (legit, suspicious, or unknown) with a confidence level and reasons. The verdict alone does not say which domain authenticated: a fully authenticated email from any domain returns legit.

IsTrustedSender anchors the verdict to an expected From domain, for handlers that gate an action on "this really came from our domain":

trust, err := primitive.IsTrustedSender(email.Raw, primitive.TrustedSenderOptions{
    Domain: "example.com",
})
if err != nil {
    // invalid options
}
switch {
case trust.Trusted:
    // authenticated mail whose From address is @example.com
case trust.Retryable:
    // transient DNS failure during DMARC evaluation; respond 5xx so
    // webhook redelivery retries this email later
default:
    log.Printf("untrusted: %s %v", trust.Reason, trust.Auth.Reasons)
}

Trusted is true only when the verdict is legit, the domain DMARC evaluated equals Domain, and the From header strict-parses to a single valid address in Domain (exactly matching Sender when given). Do not authorize based on email.ReplyTarget or email.Raw.Email.SMTP.MailFrom (both sender-controlled), and note that the normalized email.Sender is parsed leniently for display and falls back to the SMTP envelope sender, so it is not a safe authorization anchor.

x402 payments

The x402 client lets one agent request a USDC payment and another pay it. It is non-custodial: the payer signs an EIP-3009 transferWithAuthorization locally with their own key, and the key never leaves the caller. The platform resolves the real payee address, verifies every signed field against its own records, enforces the org's spend policy, and settles on chain.

The model in four steps:

  1. The payee registers a payout address once (proving control of it with a local signature).
  2. The payee creates a challenge with Charge, which the platform fills in with the registered payout address.
  3. The payer signs the challenge locally and submits it with Pay.
  4. The platform verifies and settles.

Amounts can be given as a human USDC string (AmountUsdc: "0.01") or as token base units (Amount: "10000", since USDC has 6 decimals). Networks are base (mainnet) and base-sepolia (testnet). A PrivateKeySigner built from a hex private key holds the wallet key in process and signs both the EIP-712 payment authorization (for Pay) and the ownership message (for RegisterPayoutAddress); the key is never sent to Primitive.

Build the client with NewX402Client. With zero options it reads PRIMITIVE_API_KEY from the environment and targets the production host.

client := primitive.NewX402Client(primitive.X402ClientOptions{
	APIKey: os.Getenv("PRIMITIVE_API_KEY"),
})
Register a payout address (payee, one time)

The signer proves control of its own address with an ownership message; the recovered address becomes your default payout destination for that network. Charge resolves its PayTo from this directory, so register before requesting payments. The org is resolved automatically from your API key, so you do not set it (set Org only to override the default).

payee, err := primitive.NewPrivateKeySigner(os.Getenv("PAYEE_KEY"))
if err != nil {
	log.Fatal(err)
}

label := "treasury"
_, err = client.RegisterPayoutAddress(ctx, primitive.X402PayoutRegistrationInput{
	Network: "base-sepolia",
	Label:   &label,
}, payee)
Create a challenge (payee)
challenge, err := client.Charge(ctx, primitive.X402ChargeInput{
	AmountUsdc:  "0.01", // human USDC amount
	Network:     "base-sepolia",
	PayerOrg:    os.Getenv("PAYER_ORG_ID"), // org allowed to pay
	Description: "API call",
})

Set exactly one of AmountUsdc (a human USDC string like "0.01") or Amount (token base units, e.g. "10000"). AmountUsdc is the easy path; Amount remains available when you already have a base-unit value.

Hand the returned *X402Challenge to the payer over any out-of-band channel. client.GetChallenge(ctx, id) re-hydrates a challenge by id, for example to retry Pay after a restart.

Pay a challenge (payer)

The payer signs the interaction-bound authorization locally and submits it. The key never leaves the caller.

payer, err := primitive.NewPrivateKeySigner(os.Getenv("PAYER_KEY"))
if err != nil {
	log.Fatal(err)
}

receipt, err := client.Pay(ctx, challenge, payer)
if err != nil {
	log.Fatal(err)
}
log.Println(receipt.Status, receipt.SettleTx) // settled, on-chain tx hash
Email-native payments

The challenge can also ride a real email thread instead of a synthetic id. The payee issues it as an email; the payer signs it into an interaction.json payment step and sends it back attached to the reply.

The payee issues the challenge with CreateEmailChallenge. The pay_to payout wallet and the token asset are resolved server-side; you only supply the addresses, amount, and network:

issued, err := client.CreateEmailChallenge(ctx, primitive.X402EmailChargeInput{
	From:       "payee@your-domain.example", // your sending address (funds receiver)
	To:         "payer@their-domain.example", // the payer's address
	AmountUsdc: "0.01",
	Network:    "base-sepolia",
})
if err != nil {
	log.Fatal(err)
}
// issued.InteractionID is the email thread the payment is bound to;
// issued.Challenge carries the payment_requirements + nonce_binding to sign.

The payer receives the challenge as an interaction.json MIME part on an inbound email. Rather than hand-parsing it, pass the part bytes to ExtractEmailChallenge, which validates the envelope and returns the typed *X402EmailChallenge:

// interactionPart is the body of the inbound email's `interaction.json`
// attachment.
issued, err := primitive.ExtractEmailChallenge(interactionPart)
if err != nil {
	log.Fatal(err)
}

The payer then signs the challenge locally with PayEmailChallenge and replies with the resulting envelope attached. PayEmailChallenge does not send anything; it returns the signed payment-step envelope and its canonical JSON bytes. The validity window is computed and clamped into the accepted band for you, so you never hand-set ValidBefore:

payer, err := primitive.NewPrivateKeySigner(os.Getenv("PAYER_KEY"))
if err != nil {
	log.Fatal(err)
}

built, err := client.PayEmailChallenge(issued, payer)
if err != nil {
	log.Fatal(err)
}

// built.JSON is the interaction.json body. The payer received the challenge as
// an inbound email; reply to it with the envelope attached as `interaction.json`
// using the email client's Reply method (see above). The platform reads the
// envelope, re-derives the interaction-bound nonce, and settles on chain.
_, err = client.Reply(ctx, challengeEmail, primitive.ReplyParams{
	BodyText: "Payment attached.",
	Attachments: []primitive.SendAttachment{
		{
			Filename:      "interaction.json",
			ContentBase64: base64.StdEncoding.EncodeToString([]byte(built.JSON)),
		},
	},
})
if err != nil {
	log.Fatal(err)
}
Signing primitives (lower level)

Pay builds and signs the payment for you. When you need to drive the signing yourself, for example to sign a challenge carried in an email reply and submit the payment separately, the same building blocks are exported directly:

  • DeriveEIP3009Nonce(binding) derives the interaction-bound EIP-3009 nonce, locked to a normative vector the platform recomputes.
  • ExtractEmailChallenge(part) validates an inbound interaction.json challenge part (its raw bytes) and returns the typed *X402EmailChallenge ready for PayEmailChallenge, so you never hand-parse the envelope.
  • ComputePaymentValidityWindow(input) returns the (validAfter, validBefore) window, landed inside the band the platform accepts by default: validBefore keeps at least a minimum settlement headroom (60s) so a near-expired challenge is not signed into a guaranteed rejection, and the total window is clamped to the 24h cap so a far-future expiry never produces an "authorization window too wide" rejection. Pin ValidBeforeSec/ValidAfterSec to set a bound; with Clamp pointing to false an out-of-band pinned value returns a specific error naming which bound was violated instead of silently signing a doomed authorization.
  • SignInteractionPayment(input) derives the bound nonce, assembles the authorization, and signs it with your Sign callback. The key never leaves the caller.
  • BuildExactEvmPaymentPayload(network, authorization, signature) assembles the exact-EVM x402 wire payload.
payer, err := primitive.NewPrivateKeySigner(os.Getenv("PAYER_KEY"))
if err != nil {
	log.Fatal(err)
}
pr := challenge.PaymentRequirements
expiresAt, _ := time.Parse(time.RFC3339Nano, challenge.ExpiresAt)

validAfter, validBefore, err := primitive.ComputePaymentValidityWindow(primitive.ValidityWindowInput{
	ChallengeExpiresAtSec: expiresAt.Unix(),
	NowSec:                time.Now().Unix(),
})
if err != nil {
	log.Fatal(err)
}

amount, _ := new(big.Int).SetString(pr.MaxAmountRequired, 10)
auth, signature, err := primitive.SignInteractionPayment(primitive.SignInteractionPaymentInput{
	Sign:  payer.SignTypedData,
	Payer: payer.Address(),
	Domain: primitive.TokenDomain{
		Name:              pr.Extra.Name,
		Version:           pr.Extra.Version,
		ChainID:           84532, // base-sepolia
		VerifyingContract: pr.Asset,
	},
	PayTo:  pr.PayTo,
	Amount: amount,
	NonceBinding: primitive.NonceBinding{
		InteractionID:   challenge.NonceBinding.InteractionID,
		ChallengeStepID: challenge.NonceBinding.ChallengeStepID,
		ChallengeNonce:  challenge.NonceBinding.ChallengeNonce,
	},
	ValidAfter:  validAfter,
	ValidBefore: validBefore,
})
if err != nil {
	log.Fatal(err)
}

payment, err := primitive.BuildExactEvmPaymentPayload(challenge.Network, auth, signature)
if err != nil {
	log.Fatal(err)
}
// submit `payment` to /v1/x402/challenges/{id}/pay
Read and set the spend policy

The spend policy guards outbound payments: a Paused kill-switch, per-payment and daily caps (token base units, or nil for no cap), and a payee allowlist (nil means any on-net payee, an empty slice denies all). SetSpendPolicy merges: only the fields you set on the update change, and omitted fields keep their current value. Use the builder methods on X402SpendPolicyUpdate, and ClearMaxPerPayment / ClearMaxPerDay to remove a cap.

var update primitive.X402SpendPolicyUpdate
update.SetPaused(false).SetMaxPerPayment("5000000")

policy, err := client.SetSpendPolicy(ctx, update)
if err != nil {
	log.Fatal(err)
}
_ = policy

addresses, err := client.ListPayoutAddresses(ctx)
Errors

Every method returns a *primitive.X402Error on a client-side, transport, or non-2xx server error. Use errors.As to inspect it. It carries Status (the HTTP status, or 0 for a request that never reached the server), Body (the parsed error envelope when present), and RetryAfter (the Retry-After header, when the server sent one). On Pay, a Status == 0 error means the request may not have been sent, so the payment outcome is indeterminate.

Advanced usage

Generated API package

Use the sibling api package when you want the full generated HTTP API surface.

import primitiveapi "github.com/primitivedotdev/sdks/sdk-go/api"

client, err := primitiveapi.NewAPIClient("prim_test")
if err != nil {
	log.Fatal(err)
}

res, err := client.SetMemory(ctx, &primitiveapi.SetMemoryInput{
	Key:   "greeting",
	Value: primitiveapi.NewStringMemoryJsonValue("hello"),
}, primitiveapi.SetMemoryParams{})
if err != nil {
	log.Fatal(err)
}
_ = res

Primitive Memories store durable JSON values by key. Calls default to org scope. Function-scoped memories use the function id UUID, not the function name. The generated memory methods are SetMemory, GetMemory, DeleteMemory, and SearchMemories.

Payment and interaction webhook events

Webhooks are not email-only. The same endpoint also receives payment.* settlement notifications and interaction.x402.* events from the x402-over-email flow. The event name is carried in the X-Webhook-Event header for every family. The body is sent verbatim with no envelope, so it is the header (not a body field) that names the event: an email.* body carries event, a payment.* body carries the name in type, and an interaction.* body is just {"interaction": {...}} with no event/type field at all.

HandleWebhookEvent(...) verifies the signature over the raw body first, then keys on the header to return a typed event for known types and an UnknownEvent (it does not error) for the rest:

event, err := primitive.HandleWebhookEvent(primitive.HandleWebhookOptions{
	Body:    rawBody,
	Headers: req.Header,
	Secret:  os.Getenv("PRIMITIVE_WEBHOOK_SECRET"),
})
if err != nil {
	// signature/verification failure
}

switch {
case primitive.IsPaymentSettledEvent(event):
	settled := event.(primitive.PaymentEvent) // flat fields; amount in base units
	log.Println(settled.ChallengeID, settled.Amount, settled.SettleTx)
case primitive.IsInteractionX402Event(event):
	x402 := event.(primitive.InteractionEvent) // interaction.x402.* lifecycle
	_ = x402
}

The full catalog of header values is exported as the WebhookEventTypes slice:

  • email.received, email.bounced, email.tls_report, email.dmarc_report, email.dmarc_failure
  • payment.settled, payment.failed
  • interaction.x402.challenge, interaction.x402.payment, interaction.x402.settled, interaction.x402.rejected, interaction.x402.declined, interaction.x402.expired, interaction.x402.verify_timeout
  • interaction.ack.received, interaction.ack.requested, interaction.ack.acked, interaction.ack.canceled, interaction.ack.expired

Signature verification runs on the raw body and is independent of the event type, so it works identically for payment.* and interaction.* bodies. Each delivery is signed with the dual-header scheme: the primary Primitive-Signature header and a legacy MyMX-Signature header carrying the same value. HandleWebhook(...) remains hard-typed to email.received for backward compatibility; reach for HandleWebhookEvent(...) when you need the full event union.

Lower-level webhook helpers

Advanced users can still work directly with:

  • HandleWebhook(...)
  • HandleWebhookEvent(...)
  • ParseWebhookEvent(...) (pass the X-Webhook-Event value as the optional second argument)
  • VerifyWebhookSignature(...)

Development

From sdks/sdk-go:

go test ./...
go test -run TestSharedCompatibilityFixtures ./...
gofmt -w .

Or from repo root sdks/:

make go-generate
make go-check
make go-build

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

View Source
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"
)
View Source
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.

View Source
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.

View Source
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.

View Source
const DefaultX402BaseURL = "https://api.primitive.dev"

DefaultX402BaseURL is the production API host for x402 operations. Mirrors the Node SDK's DEFAULT_BASE_URL.

Variables

View Source
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).

View Source
var EmailReceivedEventJSONSchema map[string]any
View Source
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>.

View Source
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.",
	},
}
View Source
var PaymentEventTypes = []string{
	"payment.settled",
	"payment.failed",
}

PaymentEventTypes are the two x402 settlement-notification events (subject = a payment).

View Source
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.",
	},
}
View Source
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.",
	},
}
View Source
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 BuildForwardSubject(subject string) string

func BuildPayoutRegistrationMessage added in v1.6.0

func BuildPayoutRegistrationMessage(org, address, network, issuedAt string) string

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 BuildReplySubject(subject string) string

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 ConfirmedHeaders() map[string]string

func DecodeRawEmail

func DecodeRawEmail(event any, verify ...bool) ([]byte, error)

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 GetDownloadTimeRemaining

func GetDownloadTimeRemaining(event any, nowMillis ...int64) (int64, error)

func IsDownloadExpired

func IsDownloadExpired(event any, nowMillis ...int64) (bool, error)

func IsEmailReceivedEvent

func IsEmailReceivedEvent(event any) bool

func IsEmailReceivedEventType added in v1.8.0

func IsEmailReceivedEventType(event any) bool

IsEmailReceivedEventType reports whether event names the email.received event.

func IsInteractionX402Event added in v1.8.0

func IsInteractionX402Event(event any) bool

IsInteractionX402Event reports whether event is any interaction.x402.* event.

func IsKnownWebhookEventType added in v1.8.0

func IsKnownWebhookEventType(eventType string) bool

IsKnownWebhookEventType reports whether eventType is a known current catalog value.

func IsPaymentEvent added in v1.8.0

func IsPaymentEvent(event any) bool

IsPaymentEvent reports whether event is any payment.* event.

func IsPaymentFailedEvent added in v1.8.0

func IsPaymentFailedEvent(event any) bool

IsPaymentFailedEvent reports whether event is the payment.failed event.

func IsPaymentSettledEvent added in v1.8.0

func IsPaymentSettledEvent(event any) bool

IsPaymentSettledEvent reports whether event is the payment.settled event.

func IsRawIncluded

func IsRawIncluded(event any) (bool, error)

func ParseJSONBody

func ParseJSONBody(rawBody any) (any, error)

func PrepareStandardWebhooksSecret

func PrepareStandardWebhooksSecret(secret any) ([]byte, error)

PrepareStandardWebhooksSecret strips the "whsec_" prefix if present, then base64-decodes the remainder to produce the raw HMAC key bytes.

func VerifyRawEmailDownload

func VerifyRawEmailDownload(downloaded []byte, event any) ([]byte, error)

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
}

func (*APIError) Error added in v0.8.0

func (e *APIError) Error() string

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

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 DKIMSignature struct {
	Domain   string     `json:"domain"`
	Selector *string    `json:"selector,omitempty"`
	Result   DkimResult `json:"result"`
	Aligned  bool       `json:"aligned"`
	KeyBits  *int64     `json:"keyBits,omitempty"`
	Algo     *string    `json:"algo,omitempty"`
}

type DMARCPolicyPublished added in v1.4.0

type DMARCPolicyPublished struct {
	Domain *string `json:"domain"`
	P      *string `json:"p"`
	SP     *string `json:"sp"`
	Pct    *int64  `json:"pct"`
	Adkim  *string `json:"adkim"`
	Aspf   *string `json:"aspf"`
}

type DMARCRecord added in v1.4.0

type DMARCRecord struct {
	SourceIP    *string `json:"source_ip"`
	Count       int64   `json:"count"`
	Disposition *string `json:"disposition"`
	DKIM        *string `json:"dkim"`
	SPF         *string `json:"spf"`
	HeaderFrom  *string `json:"header_from"`
}

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 Delivery

type Delivery struct {
	EndpointID  string `json:"endpoint_id"`
	Attempt     int64  `json:"attempt"`
	AttemptedAt string `json:"attempted_at"`
}

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 DownloadInfo struct {
	URL       string `json:"url"`
	ExpiresAt string `json:"expires_at"`
}

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 EmailAddress struct {
	Address string  `json:"address"`
	Name    *string `json:"name"`
}

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 EmailHeaders struct {
	MessageID *string `json:"message_id"`
	Subject   *string `json:"subject"`
	From      string  `json:"from"`
	To        string  `json:"to"`
	Date      *string `json:"date"`
}

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 ErrorDefinition struct {
	Message    string
	Suggestion string
}

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 ForwardOriginalSender struct {
	Email  string `json:"email"`
	Domain string `json:"domain"`
}

type ForwardParams added in v0.8.0

type ForwardParams struct {
	To             string
	BodyText       string
	Subject        string
	From           string
	IdempotencyKey string
}

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 HandleWebhookOptions struct {
	Body             any
	Headers          any
	Secret           any
	ToleranceSeconds *int64
}

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

type InteractionEvent struct {
	Event   string         `json:"event"`
	Payload map[string]any `json:"-"`
}

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 ParsedError struct {
	Code      string `json:"code"`
	Message   string `json:"message"`
	Retryable bool   `json:"retryable"`
}

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 ReceiveRequestOptions struct {
	Secret           string
	ToleranceSeconds *int64
}

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

type ReceivedEmailAddress struct {
	Address string
	Name    string
}

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 ReceivedEmailThread struct {
	MessageID  string
	InReplyTo  []string
	References []string
}

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

type ReportDateRange struct {
	Start *string `json:"start"`
	End   *string `json:"end"`
}

ReportDateRange is the reporting window shared by TLS and DMARC reports.

type SMTPEnvelope

type SMTPEnvelope struct {
	Helo     *string  `json:"helo"`
	MailFrom string   `json:"mail_from"`
	RcptTo   []string `json:"rcpt_to"`
}

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 SendParams struct {
	From           string
	To             string
	Subject        string
	BodyText       string
	BodyHTML       string
	Thread         *SendThread
	Wait           *bool
	WaitTimeoutMs  int
	IdempotencyKey string
}

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 SendThread struct {
	InReplyTo  string
	References []string
}

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 SpfResult

type SpfResult string
const (
	SpfResultPass      SpfResult = "pass"
	SpfResultFail      SpfResult = "fail"
	SpfResultSoftfail  SpfResult = "softfail"
	SpfResultNeutral   SpfResult = "neutral"
	SpfResultNone      SpfResult = "none"
	SpfResultTemperror SpfResult = "temperror"
	SpfResultPermerror SpfResult = "permerror"
)

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 StandardWebhooksVerifyOptions

type StandardWebhooksVerifyOptions struct {
	RawBody          any
	MsgID            string
	Timestamp        string
	SignatureHeader  string
	Secret           any
	ToleranceSeconds *int64
	NowSeconds       *int64
}

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 TLSReportFailure struct {
	ResultType          *string `json:"result_type"`
	Count               int64   `json:"count"`
	SendingMTAIP        *string `json:"sending_mta_ip"`
	ReceivingMXHostname *string `json:"receiving_mx_hostname"`
}

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

type TokenDomain struct {
	Name              string
	Version           string
	ChainID           int64
	VerifyingContract string
}

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:

  1. ValidateEmailAuth(event.Email.Auth) returns a legit verdict.
  2. event.Email.Auth.DMARCFromDomain (the domain the server's DMARC evaluation ran against) equals opts.Domain.
  3. The From header strict-parses to exactly one valid address whose domain equals opts.Domain.
  4. 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 ValidationIssue struct {
	Path      string `json:"path"`
	Message   string `json:"message"`
	Validator string `json:"validator"`
}

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 VerifyOptions struct {
	RawBody          any
	SignatureHeader  string
	Secret           any
	ToleranceSeconds *int64
	NowSeconds       *int64
}

type WebhookAttachment

type WebhookAttachment struct {
	Filename    *string `json:"filename"`
	ContentType string  `json:"content_type"`
	SizeBytes   int64   `json:"size_bytes"`
	SHA256      string  `json:"sha256"`
	PartIndex   int64   `json:"part_index"`
	TarPath     string  `json:"tar_path"`
}

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.

func (*X402Error) Error added in v1.6.0

func (e *X402Error) Error() string

func (*X402Error) Unwrap added in v1.6.0

func (e *X402Error) Unwrap() error

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

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

SetPaused sets the kill-switch field on the update.

Directories

Path Synopsis
Package api provides a generated client for the Primitive HTTP API.
Package api provides a generated client for the Primitive HTTP API.

Jump to

Keyboard shortcuts

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