primitive

package module
v1.36.0 Latest Latest
Warning

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

Go to latest
Published: Oct 6, 2026 License: MIT Imports: 38 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.26

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 <[email protected]>",
	To:      "[email protected]",
	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,
})

With Wait set to true, Send and Reply keep the HTTP request open until Primitive's downstream SMTP transaction completes; when Wait is unset the call returns as soon as the API accepts the message. Forward has no Wait option and always returns on acceptance. When waiting, use a context deadline long enough for SMTP delivery, typically 30-60 seconds.

Per-call timeout and cancellation

Every client method that performs a network request 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. The one exception is X402Client.PayEmailChallenge, which signs a payment locally without any I/O and therefore takes no context.

// 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 <[email protected]>",
    To:      "[email protected]",
    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:     "[email protected]",
})
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 (the email was rejected, its content was discarded, or no recipient was recorded), the API returns inbound_not_repliable (HTTP 422) and the SDK returns an error. A missing Message-Id does not block the reply; it only omits the threading headers.

Forward an inbound email
_, err = client.Forward(context.Background(), email, primitive.ForwardParams{
	To:       "[email protected]",
	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 requires a legit verdict and a strict-parsed single From address in the exact expected Domain (and exact 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.

The reported DMARC domain can be an organizational domain such as example.com for mail from [email protected]. When it differs from the expected From domain, the helper requires DMARC pass and a passing, aligned DKIM signature from that exact expected domain. For a managed inbox such as [email protected], it also accepts primitive.email as the signer, relying on Primitive to authorize the sending identity. A sibling signer, an arbitrary parent signer, or SPF alone cannot satisfy this exception. The qualifying signature must use RSA-SHA256 with a reported key size of at least 1024 bits, or Ed25519-SHA256. Missing RSA key size or an unknown algorithm fails closed on this path. Continue passing the full subdomain as the expected domain.

Use this helper only with a verified Primitive webhook or an event obtained through the authenticated API. It consumes the server's authentication results; it does not verify DKIM or the webhook signature itself.

Interaction envelopes

Use ParseInteractionEnvelope([]byte) or ParseInteractionEnvelopeString(string) to read an interaction.json attachment for display. The bounded parser returns valid, unsupported, or invalid and preserves unknown protocols. It performs no authentication, network requests, payments, or sends. See the parser contract and limits.

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:       "[email protected]", // your sending address (funds receiver)
	To:         "[email protected]", // 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 once; the same t=...,v1=... value is sent on the primary Primitive-Signature header plus X-Primitive-Signature and X-Webhook-Signature for non-SDK consumers. The SDK verifies Primitive-Signature, and still accepts the retired MyMX-Signature header name when verifying older captured deliveries. 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

Delete mailbox history

Use DeleteSentEmail and RemoveAgentConnection on the generated api.Client. Sent-mail deletion removes sender history and owned attachments, not recipient copies or delivery already admitted. Cancel scheduled sends first; ineligible states return 409. Retrying a completed DELETE succeeds. After 500 or 503, files may already be removed; retry the same DELETE.

Connection removal requires an owner or admin session or OAuth token and a previously revoked connection. It preserves mail, notes and the external runtime. Active connections return 409; missing records return 404.

Send and reply preserve HTTP 410 sent_email_deleted and its details.idempotent_replay value. This is a refusal to resend deleted history; do not replace the idempotency key or retry as a fresh message to bypass it.

Receive events in your process

client, err := primitive.NewClient(apiKey)
if err != nil { return err }
listener, err := client.Events.Listen(ctx, func(ctx context.Context, event primitive.LocalEvent) error {
    return app.Receive(ctx, event) // Accept or enqueue within 30 seconds.
}, primitive.EventOptions{Subscription: "my-agent", Events: []string{"email.received"}})
if err != nil { return err }
return listener.Wait()
// During application shutdown: listener.Close(shutdownContext)

The SDK registers the named subscription and reconnects over WebSocket. Listen returns once ready and then runs alongside the application. Returning nil from the handler accepts responsibility. The handler context is canceled on its 30-second acceptance deadline. Longer work should run after enqueueing.

delivery, err := client.Events.Wait(ctx, primitive.EventOptions{Subscription: "my-agent"})
if err != nil { return err }
if err := app.Receive(delivery.Context, delivery.Event); err != nil {
    return delivery.Retry()
}
return delivery.Ack()

Use a context deadline to bound waiting; timeout returns context.DeadlineExceeded. Wait does not acknowledge before the application runs. LocalEvent preserves the exact Body and Headers, canonical ID and Type, and raw JSON Data. Decode parses Data into an application or SDK event type. Delivery is at least once; deduplicate side effects by ID when needed. Same name shares work; different names receive independent copies. Closing preserves the queue. New names do not backfill. OnStatus reports reconnects, handler failures, and gaps; OnGapError stops on a gap. Set Transport to "poll" explicitly when WebSocket access is unavailable.

Connected-agent listeners

Use the agent's existing connected-address credential with the same listener API. The server automatically restricts its private subscription to inbound email for that address. No owner credential, recipient filter, or relay is required. Names are isolated per credential. Revoking or replacing the credential removes its subscriptions and queued deliveries; reconnect with the new credential to start receiving new events.

Address-scoped events contain parsed message content in email.parsed. email.content.raw and email.content.download are null. Signed download links, account routing metadata, and other SMTP envelope recipients are not exposed. Attachments can be fetched through the authenticated email attachment API.

For typed email events, event.Email.Content.Restricted reports unavailable raw content and downloads. The existing Raw and Download value fields remain compatible with struct literals; JSON round trips preserve restricted fields as null.

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 (
	RepeatTickProtocol    = "repeat.tick"
	RepeatStopProtocol    = "repeat.stop"
	RepeatProtocolVersion = 1
	RepeatTickStep        = "tick"
	RepeatStopStep        = "stop"
	RepeatTickKind        = "repeat.tick/1"
	RepeatStopKind        = "repeat.stop/1"
	RepeatStopReasonMax   = 280 // UTF-16 code units; astral characters count twice.
	RepeatEveryMinMinutes = 1
	RepeatEveryMaxMinutes = 10080
	RepeatIdleMaxMinutes  = 10080
)

Repeating sends: repeat.tick/1 and repeat.stop/1.

A send or reply with repeat goes out now and then again every few minutes in the same thread. Each message carries a repeat.tick/1 interaction part. When the repeat allows it, the recipient stops it through POST /v1/emails/{id}/repeat-stop, and the server replies in the thread with a repeat.stop/1 part for the sender. These helpers only parse and build data; they make no requests. A parsed part is not proof that Primitive sent the message: use the server-verified repeat marker on the email for that.

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.

View Source
const MaxInteractionBytes = 65536
View Source
const MaxInteractionDepth = 64
View Source
const MaxPresenceDecodedBytes = 8192
View Source
const MaxPresenceEnvelopeBytes = 4096
View Source
const MaxPresenceRenderedBytes = 16384
View Source
const PresenceAliveSubject = "Re: Receiver presence check"
View Source
const PresenceAliveText = "This receiver answered the presence check."
View Source
const PresenceProbeSubject = "Receiver presence check"
View Source
const PresenceProbeText = "This email checks whether your receiver is available."
View Source
const PresenceProtocol = "primitive.presence"
View Source
const PresenceTTLMS int64 = 600000
View Source
const PresenceVersion = 1

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 ErrDeliveryExpired = &EventReceiverError{Code: "delivery_expired", Status: 409}
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{
	"UNAVAILABLE": {
		Message:    "Raw email content is unavailable for this credential",
		Suggestion: "Use event.email.parsed for message content. This credential cannot download raw email.",
	},
	"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 BuildRepeatStopBody added in v1.35.0

func BuildRepeatStopBody(reason string) (map[string]string, error)

BuildRepeatStopBody returns the request body for POST /v1/emails/{id}/repeat-stop.

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 InteractionKind added in v1.35.0

func InteractionKind(envelope map[string]any) string

InteractionKind returns protocol/protocol_version, e.g. repeat.tick/1.

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 NormalizeRepeatStopReason added in v1.35.0

func NormalizeRepeatStopReason(reason string) (string, error)

NormalizeRepeatStopReason trims ASCII whitespace and returns "" for no reason. It rejects control characters, invalid UTF-8 and reasons longer than 280 UTF-16 code units.

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 RepeatStopCommand added in v1.35.0

func RepeatStopCommand(emailID string) (string, error)

RepeatStopCommand returns the CLI command that stops the repeat behind a received message.

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 {
	Events *EventsResource
	// 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:

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. In Primitive's managed service this is included only when
	// forwarded content is detected; nil otherwise.
	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"`
	// Restricted reports that this credential cannot access raw content or downloads.
	// Raw and Download retain their value types for existing struct constructors.
	Restricted bool `json:"-"`
}

func (EmailContent) MarshalJSON added in v1.29.0

func (c EmailContent) MarshalJSON() ([]byte, error)

func (*EmailContent) UnmarshalJSON added in v1.29.0

func (c *EmailContent) UnmarshalJSON(data []byte) error

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 EventHandler added in v1.27.0

type EventHandler func(context.Context, LocalEvent) error

type EventListener added in v1.27.0

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

func (*EventListener) Close added in v1.27.0

func (l *EventListener) Close(ctx context.Context) error

func (*EventListener) Wait added in v1.27.0

func (l *EventListener) Wait() error

type EventOptions added in v1.27.0

type EventOptions struct {
	Subscription string
	Events       []string
	// Transport defaults to websocket. Set poll explicitly for HTTP polling.
	Transport  string
	OnStatus   func(EventStatus)
	OnGapError bool
	// Dial supports specialized proxy or TLS setup. The default retains the API HTTP client.
	Dial func(context.Context, string, *websocket.DialOptions) (*websocket.Conn, *http.Response, error)
}

type EventReceiverError added in v1.27.0

type EventReceiverError struct {
	Code       string
	Status     int
	RetryAfter time.Duration
}

func (*EventReceiverError) Error added in v1.27.0

func (e *EventReceiverError) Error() string

type EventStatus added in v1.27.0

type EventStatus struct {
	Type          string
	Backlog       int
	GapCount      int
	LastGapReason *string
	Err           error
}

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 EventsResource added in v1.27.0

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

func (*EventsResource) Listen added in v1.27.0

func (r *EventsResource) Listen(ctx context.Context, handler EventHandler, options EventOptions) (*EventListener, error)

func (*EventsResource) Wait added in v1.27.0

func (r *EventsResource) Wait(ctx context.Context, options EventOptions) (*PendingEvent, error)

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
	// ScheduledAt is an optional future execution time, forwarded
	// through to the underlying send. Same semantics and
	// server-enforced bounds as SendParams.ScheduledAt.
	ScheduledAt time.Time
}

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 InteractionResult added in v1.24.0

type InteractionResult struct {
	Status   string
	Envelope map[string]any
	Version  int64
	Reason   string
	Source   []byte
}

InteractionResult describes syntax and envelope shape only, never authenticity. Envelope retains unknown fields and protocols. Source is a defensive exact copy for parsed valid/unsupported input; decoded validation never supplies it.

func ParseInteractionEnvelope added in v1.24.0

func ParseInteractionEnvelope(source []byte) InteractionResult

ParseInteractionEnvelope parses at most 64 KiB of strict UTF-8 JSON. For string input use ParseInteractionEnvelopeString.

func ParseInteractionEnvelopeString added in v1.24.0

func ParseInteractionEnvelopeString(source string) InteractionResult

func ValidateInteractionEnvelope added in v1.24.0

func ValidateInteractionEnvelope(value any) InteractionResult

ValidateInteractionEnvelope validates decoded JSON maps/slices/scalars only. It cannot detect original duplicate keys or recover original bytes.

type LocalEvent added in v1.27.0

type LocalEvent struct {
	ID      string
	Type    string
	Data    json.RawMessage
	Body    string
	Headers map[string]string
}

func (LocalEvent) Decode added in v1.27.0

func (e LocalEvent) Decode(target interface{}) error

Decode provides typed access while preserving unknown future event payloads.

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 PendingEvent added in v1.27.0

type PendingEvent struct {
	Event   LocalEvent
	Context context.Context
	// contains filtered or unexported fields
}

func (*PendingEvent) Ack added in v1.27.0

func (d *PendingEvent) Ack() error

func (*PendingEvent) Retry added in v1.27.0

func (d *PendingEvent) Retry() error

type PreparedPresence added in v1.34.0

type PreparedPresence struct {
	AccountScope   string `json:"accountScope"`
	PreparedAtMs   int64  `json:"preparedAtMs"`
	ExpiresAtMs    int64  `json:"expiresAtMs"`
	IdempotencyKey string `json:"idempotencyKey"`
	RequestJSON    string `json:"requestJson"`
}

PreparedPresence is a value snapshot. Persist it unchanged before dispatch.

type PreparedSignal added in v1.24.0

type PreparedSignal struct {
	AccountScope   string `json:"accountScope"`
	PreparedAtMs   int64  `json:"preparedAtMs"`
	ExpiresAtMs    *int64 `json:"expiresAtMs"`
	IdempotencyKey string `json:"idempotencyKey"`
	RequestJSON    string `json:"requestJson"`
}

PreparedSignal is a value snapshot. Persist all fields before dispatch; do not change fields between retries. RequestJSON fixes the ordinary send body.

type PresenceAliveInput added in v1.34.0

type PresenceAliveInput struct {
	PresenceProbeInput
	Probe      PresenceEnvelope
	MessageID  *string
	References []string
}

type PresenceDependencies added in v1.34.0

type PresenceDependencies struct {
	UUID func() string
	Now  func() int64
}

type PresenceEnvelope added in v1.34.0

type PresenceEnvelope 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         *string         `json:"prev_step_id"`
	ExpiresAt          string          `json:"expires_at"`
	Payload            PresencePayload `json:"payload"`
}

type PresenceParseResult added in v1.34.0

type PresenceParseResult struct {
	Status   string
	Envelope *PresenceEnvelope
	Version  int64
	Reason   string
	Source   []byte
}

PresenceParseResult describes syntax only, including expired controls.

func ParsePresenceEnvelope added in v1.34.0

func ParsePresenceEnvelope(source []byte) PresenceParseResult

ParsePresenceEnvelope authenticates nothing and consults no clock.

func ParsePresenceEnvelopeString added in v1.34.0

func ParsePresenceEnvelopeString(source string) PresenceParseResult

type PresencePayload added in v1.34.0

type PresencePayload struct {
	Nonce    string `json:"nonce"`
	IssuedAt string `json:"issued_at"`
	Address  string `json:"address"`
}

type PresencePreparation added in v1.34.0

type PresencePreparation struct {
	Status   string            `json:"status"`
	Prepared *PreparedPresence `json:"prepared,omitempty"`
}

func PreparePresenceAliveEmail added in v1.34.0

func PreparePresenceAliveEmail(input PresenceAliveInput, dependencies PresenceDependencies) (PresencePreparation, error)

PreparePresenceAliveEmail requires caller-verified origin, binding and server freshness.

func PreparePresenceProbeEmail added in v1.34.0

func PreparePresenceProbeEmail(input PresenceProbeInput, dependencies PresenceProbeDependencies) (PresencePreparation, error)

PreparePresenceProbeEmail creates one fixed ordinary send body without IO.

type PresenceProbeDependencies added in v1.34.0

type PresenceProbeDependencies struct {
	PresenceDependencies
	Nonce func() string
}

type PresenceProbeInput added in v1.34.0

type PresenceProbeInput struct{ AccountScope, From, To string }

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 RepeatStop added in v1.35.0

type RepeatStop struct {
	Reason *string `json:"reason"`
}

RepeatStop is the repeat.stop/1 payload. Reason is recipient-written and untrusted.

type RepeatStopResult added in v1.35.0

type RepeatStopResult struct {
	Status string
	Stop   *RepeatStop
	Reason string
}

RepeatStopResult has Status "valid", "other" or "invalid".

func ParseRepeatStop added in v1.35.0

func ParseRepeatStop(source []byte) RepeatStopResult

ParseRepeatStop parses interaction.json bytes and reads a repeat.stop/1 payload.

func ReadRepeatStop added in v1.35.0

func ReadRepeatStop(envelope map[string]any) RepeatStopResult

ReadRepeatStop reads the stop payload from an envelope that already passed envelope validation.

type RepeatTick added in v1.35.0

type RepeatTick struct {
	RepeatID                   string `json:"repeat_id"`
	Sequence                   int64  `json:"sequence"`
	EveryMinutes               int64  `json:"every_minutes"`
	OnlyIfRecipientIdleMinutes *int64 `json:"only_if_recipient_idle_minutes"`
	StoppableByRecipient       bool   `json:"stoppable_by_recipient"`
}

RepeatTick is the repeat.tick/1 payload.

type RepeatTickResult added in v1.35.0

type RepeatTickResult struct {
	Status string
	Tick   *RepeatTick
	Reason string
}

RepeatTickResult has Status "valid", "other" or "invalid"; Reason is "invalid_envelope", "invalid_step" or "invalid_payload" when invalid.

func ParseRepeatTick added in v1.35.0

func ParseRepeatTick(source []byte) RepeatTickResult

ParseRepeatTick parses interaction.json bytes and reads a repeat.tick/1 payload.

func ReadRepeatTick added in v1.35.0

func ReadRepeatTick(envelope map[string]any) RepeatTickResult

ReadRepeatTick reads the tick payload from an envelope that already passed envelope validation.

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
	// ScheduledAt is an optional future execution time. When set, the
	// send is recorded with status "scheduled" and executed at the
	// requested time instead of immediately. Server-enforced bounds:
	// strictly in the future, at most 30 days out. Incompatible with
	// Wait (a scheduled send resolves after this request completes).
	// Reschedule via RescheduleSentEmail; cancel via CancelSentEmail
	// on the generated client.
	ScheduledAt time.Time
}

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
	// ScheduledAt echoes the requested execution time on a
	// "scheduled" response. Unset on immediate sends.
	ScheduledAt primitiveapi.OptDateTime
}

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 SignalContentBodies added in v1.24.0

type SignalContentBodies struct {
	Status string  `json:"status"`
	Text   *string `json:"text"`
	HTML   *string `json:"html"`
}

type SignalContentInput added in v1.24.0

type SignalContentInput struct {
	Inventory SignalContentInventory
	Bodies    SignalContentBodies
	// Nil means unavailable; an empty non-nil slice is a known empty part.
	CanonicalPartBytes []byte
}

type SignalContentInventory added in v1.24.0

type SignalContentInventory struct {
	// Complete asserts the full outer inventory, including offloaded parts.
	Status string              `json:"status"`
	Parts  []SignalContentPart `json:"parts"`
}

type SignalContentPart added in v1.24.0

type SignalContentPart struct {
	Filename    *string `json:"filename"`
	ContentType *string `json:"contentType"`
}

SignalContentPart describes one member of the full outer attachment inventory.

type SignalContentResult added in v1.24.0

type SignalContentResult struct {
	Classification string
	Reason         string
	Interaction    *InteractionResult
}

SignalContentResult establishes no sender, receipt or task authority.

func ClassifySignalContent added in v1.24.0

func ClassifySignalContent(input SignalContentInput) SignalContentResult

ClassifySignalContent is pure. Complete inventory/body projections are caller assertions, not evidence of authentication or completeness from this function.

type SignalDependencies added in v1.24.0

type SignalDependencies struct {
	UUID func() string
	// Now returns Unix time in integer milliseconds.
	Now func() int64
}

type SignalInput added in v1.24.0

type SignalInput struct {
	Parent      SignalParent `json:"parent"`
	Kind        string       `json:"kind"`
	Status      string       `json:"status,omitempty"`
	Note        *string      `json:"note,omitempty"`
	ExpiresAtMs *int64       `json:"expiresAtMs,omitempty"`
}

type SignalParent added in v1.24.0

type SignalParent struct {
	AccountScope string   `json:"accountScope"`
	From         string   `json:"from"`
	To           string   `json:"to"`
	MessageID    *string  `json:"messageId"`
	Subject      string   `json:"subject"`
	References   []string `json:"references"`
}

SignalParent contains explicit caller-authenticated addressing and account context.

type SignalPreparation added in v1.24.0

type SignalPreparation struct {
	Status   string          `json:"status"`
	Prepared *PreparedSignal `json:"prepared,omitempty"`
}

func PrepareSignalEmail added in v1.24.0

func PrepareSignalEmail(input SignalInput, dependencies SignalDependencies) (SignalPreparation, error)

PrepareSignalEmail builds one explicit ordinary email without performing IO.

type SignalSendResult added in v1.24.0

type SignalSendResult[T any] struct {
	Status         string
	Result         T
	IdempotencyKey string
}

SignalSendResult preserves the ordinary operation's response. An expired result is a local refusal, never evidence that an earlier attempt failed.

func SendPreparedSignal added in v1.24.0

func SendPreparedSignal[T any](ctx context.Context, sendMail func(context.Context, json.RawMessage, string) (T, error), prepared PreparedSignal, accountScope string, now func() int64) (SignalSendResult[T], error)

SendPreparedSignal makes one ordinary attempt using the supplied operation. The fresh RawMessage copy cannot alter the saved request for subsequent retries.

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
	// domain DMARC reported does not match the expected domain and no
	// qualifying subdomain DKIM signature proves its identity.
	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
	// ([email protected]). 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. The reported DMARC domain equals opts.Domain, or is its parent and passing, aligned DKIM uses the exact expected domain. A direct child of primitive.email or primitive-staging.email may instead use that same managed root as its signer, trusting Primitive to authorize the sending identity.
  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 and requiring DKIM proof for subdomain exceptions closes that. A shared organizational domain or SPF-only pass never suffices for the exception. The qualifying signature must use RSA-SHA256 with at least 1024 reported key bits, or Ed25519-SHA256. Unknown algorithms or RSA sizes fail closed. The strict From parse defends the remaining gaps: a header like `From: "[email protected]" <[email protected]>` 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