Developer docs

Integration Guide: Connect → Authorize → Spend → Settle

This guide follows the full lifecycle of an agent payment, from first connecting an agent to a settled receipt. It expands the quickstart with the ordering, the idempotency guarantees, and the failure handling that make the flow safe under retries and crashes. It is grounded in Tidal's accepted payment workflow.

The four phases

Connect establishes the agent and its key. You call createAgent(userId, agentId); the agent key is created in your key store and only its public half is registered in Tidal's directory. This is idempotent on your identifiers — a retry never mints a second key.

Authorize gets the user's consent within limits. You call requestAuthorization({ limits, currency, expiry }) and present the returned App Link / QR payload — public routing handles only — to the user. They review the limits and the verified identity of your application in their Tidal wallet, approve, and their master key provisions the budget account: funding it, setting up the RLUSD trust line if needed, and installing the two-of-two signer list. You learn it is active by polling or by the authorization.activated webhook.

Spend is the per-payment flow, driven by a single idempotent call. You provide a unique idempotencyKey; the SDK and platform carry that identity through policy approval, agent signing, co-signing, assembly, submission, and finality — described in sequence below.

Settle is what the ledger does: a direct XRP or RLUSD Payment with two signatures validates, and you receive the bound receipt. Settlement is final.

The payment sequence

The payment call runs a fixed, fail-closed workflow. Each step is durably ordered so that a crash or timeout resumes correctly rather than double-spending.

sequenceDiagram
    autonumber
    participant A as Agent backend<br/>(SDK)
    participant P as Policy &amp; risk
    participant K as Your KeyStore
    participant C as Tidal co-signer<br/>+ wallet exec
    participant X as XRP Ledger

    A->>A: Validate inputs; claim idempotency identity
    A->>A: Build owner intent + exact direct-Payment request
    A->>P: Request policy decision
    alt denied or challenge
        P-->>A: Denied / challenge — stop (no signature yet)
    else approved
        P-->>A: Signed policy approval
        A->>A: Prepare signer preimage (wallet-core); freeze tx
        A->>A: Reserve operation; bind approval + frozen tx; set signing fence
        A->>K: Sign agent half over the frozen preimage
        K-->>A: Agent Signer contribution
        A->>C: Submit signer request (agent half + approval)
        C->>C: Verify approval, fields, Payment-only; HSM co-signs
        C-->>A: Second Signer contribution
        A->>A: Assemble 2-of-2; persist bytes + tx ID before submit
        A->>X: Submit once
        X-->>A: Validated finality
        A-->>A: Return bound receipt
    end

Two things about this sequence are worth internalizing.

First, no agent signature exists until after policy approval. Denials and challenges happen at step 3–4, before your key store is ever asked to sign. A denied payment is genuinely never signed by anyone.

Second, there is a point of no return at signing. Before the signing fence, if something fails the operation can be released cleanly. From the moment the signer request is dispatched, signature material may exist, so a retry must retain the same operation, the same approval, and the same frozen transaction — it resumes the exact state or enters explicit reconciliation. It never starts a second signing or submission path. After a transaction may have been submitted, automatic retry never resubmits; an ambiguous submission reconciles by the bound transaction ID.

Idempotency in practice

idempotencyKey is mandatory, developer-generated, high-entropy, and unique within the agent. The platform binds it to a canonical request digest and a single operation:

  • Same key, same canonical request → resumes or returns the same operation.
  • Same key, different request → IdempotencyConflictError.
  • A completed retry performs no policy, signing, co-signing, or submission side effect — it returns the same receipt.
  • Concurrent callers get the same completed result or ConcurrentExecutionError, never two executions.

The practical rule for your code: generate the key once per logical payment, persist it, and reuse the same key on every retry of that payment. Do not regenerate it on error — that is precisely how you would create a duplicate.

Handling outcomes

The server SDK exports stable, typed errors. Handle them by type, not by inspecting a status code — a status code without its typed reason is treated as contract-invalid on purpose, because several payment states share one HTTP status.

ErrorMeaningWhat to do
AuthenticationErrorRequest credential invalid.Fix credential/signing; do not retry blindly.
CredentialRevokedErrorThe API credential was revoked.Rotate credentials.
PolicyDeniedErrorPayment broke a limit or policy rule; never signed.Surface the reason; do not retry as-is.
ChallengePendingErrorUser must answer a challenge.Resolve the challenge, then resume the same operation.
InsufficientBudgetErrorBudget account cannot cover it above reserve + fees.Prompt a top-up; retry after funding.
AgentKeyConflictErrorSame agent identity, different public key.Investigate; do not force a new key.
IdempotencyConflictErrorSame idempotency key, different request.Fix the key/request pairing.
ConcurrentExecutionErrorAnother execution of the same operation is in flight.Wait and read the operation; do not launch a second.
CoSignerUnavailableErrorCo-signer temporarily unavailable (retryable: true).Retry with backoff, same idempotency key.
LedgerWindowExpiredErrorThe signing ledger window passed.A fresh approval is required for a new attempt.
ReconciliationRequiredErrorAmbiguous submission.Reconcile by the bound transaction ID; never resubmit.
WorkflowAbortedErrorThe workflow was aborted before commitment.Safe to restart the logical payment.

The HTTP mapping the SDK relies on: 409 for idempotency/concurrency conflicts and for RECONCILIATION_REQUIRED; 422 for policy-denied, challenge-pending, and insufficient-budget; 503 with retryable: true for co-signer unavailability; and 202 when the same operation is still progressing and can be polled or resumed. Every error envelope carries an explicit retryable value.

Draft — to be finalized. As noted in the quickstart, the single-call agent.pay() state machine is the target. The current server SDK ships agent.initiatePayment(input) and agent.getPayment(operationId) as the reviewed building blocks over this same workflow and idempotency journal; a challenge surfaces as a challenge_pending state on initiation. The lifecycle and guarantees above are the contract; the exact surface is converging on pay().

Webhooks

Rather than poll, subscribe to the events you care about. Facts have a single owner but are all delivered over one webhook channel:

  • authorization.requested, authorization.approved, authorization.revoked, challenge.raised, payment.denied — from the policy authority.
  • authorization.activated (after the signer list is observed on-ledger), agent.created, agent.key_rotated, payment.settled (after validated finality) — Tidal facts, delivered on the same channel.

Each delivery is signed with an independent webhook secret over T54-Webhook-Id, T54-Webhook-Timestamp, and T54-Webhook-Signature (HMAC-SHA256 over id.timestamp.exact-body-bytes). Delivery is at least once, so deduplicate by event ID — but only after the signature, the timestamp window, and the envelope validate. The server SDK exports a verifyWebhook helper from its server-only entry point that does this correctly; use it rather than hand-rolling verification.

Read next

  • Security model — the custody and enforcement guarantees underneath this lifecycle.
  • Concepts — the objects referenced throughout.