paybondpaybond
Sign in

Disputes and evidence

Reference for evidence evaluation, Harbor intent states, Gateway dispute cases, chargeback signals, and arbitration export.

This document is the dense reference companion to the public guide Disputes and evidence evaluation. It describes how automated predicate evaluation differs from human dispute workflows, maps Harbor and Gateway state machines, and documents the arbitration handoff packet.

Design principles

  1. Evidence evaluation is deterministic. Harbor runs paybond-predicate-vm over signed payee evidence and the intent's frozen predicate contract. The result is stored as predicate_passed before any terminal money move.
  2. Predicate failure is not an HTTP error. submit_evidence returns 200 with predicate_passed: false; settlement/confirm drives refund when appropriate.
  3. Disputes freeze automation. Harbor disputed blocks drive_terminal_settlement until dispute/resolve. Gateway cases add timeline, notes, evidence URI refs, and export — they do not replace Harbor money authority for resolved_release / resolved_refund.
  4. Provider chargebacks are signals. Stripe charge.dispute.* ingest may set settlement_exception_* fields and Signal PROVIDER_STRIPE_DISPUTE without transitioning intent state.
  5. Tenant isolation at every hop. Gateway dispute queries filter by authenticated tenant_id. Harbor operator routes require tenant-scoped upstream bearer or mTLS client identity.

Evidence evaluation

sequenceDiagram
  autonumber
  actor Payee as Payee / Kit
  participant GW as Gateway
  participant HB as Harbor
  participant VM as Predicate VM
  participant Rail as Settlement rail

  Payee->>GW: POST /harbor/intents/{id}/evidence (tenant API key)
  GW->>HB: Forward evidence (mTLS)
  HB->>HB: Verify EvidenceSignV1 (version 2)
  HB->>VM: Evaluate predicate_dsl or policy_binding
  HB->>HB: Optional completion_schema drift report
  HB->>HB: funded → evidence_submitted
  HB-->>GW: predicate_passed, predicate_evaluation, schema_validation
  GW-->>Payee: 200 JSON body

  Payee->>GW: POST /harbor/intents/{id}/settlement/confirm
  GW->>HB: Forward confirm
  alt predicate_passed = true
    HB->>Rail: Capture / release
    HB-->>HB: released
  else predicate_passed = false
    HB->>Rail: Cancel / refund
    HB-->>HB: refunded
  end

Evidence contract inputs

InputSourceNotes
predicate_dslIntent create (signing v6)Inline JSON predicate document
policy_bindingIntent create (signing v7)Managed policy head + digest
completion_preset_idIntent createOptional; catalog frozen at create
Payee evidence payloadPOST /evidence bodyMust match evidence schema expectations
EvidenceSignV1Payee keyversion = 2, bincode 2 wire

Kit signing helpers: kit/ts/src/agent/evidence.ts, Python parity in paybond_kit.

Predicate evaluation response (Harbor)

Key fields on successful evidence submit:

FieldMeaning
predicate_passedtrue / false — business outcome
predicate_evaluationStructured VM report (rule trace)
schema_validationDrift report when preset bound; signal-only in v1
stateevidence_submitted after first successful bind

Ledger events appended include EvidenceSubmitted, PredicateEvaluated, and optionally CompletionSchemaValidated.

Schema validation (signal-only)

completion_schema.rs validates vendor and canonical shapes at submit time. Failures appear in schema_validation but do not reject the HTTP request. Operators may see passed_with_drift when the predicate passed but quality or vendor checks failed.

One-shot evidence binding

Lifecycle transition fundedevidence_submitted accepts exactly one evidence submission path per intent. Re-submission is not supported; contested outcomes use disputes.

Dispute handling

sequenceDiagram
  autonumber
  actor Op as Operator
  participant GW as Gateway
  participant PG as Postgres cases
  participant HB as Harbor
  participant Rail as Settlement rail

  Op->>GW: POST /v1/disputes/cases { intent_id, subject, ... }
  GW->>HB: POST /operator/v1/intents/{id}/dispute/open
  HB-->>HB: state → disputed
  GW->>PG: Insert case (status frozen), timeline
  GW-->>Op: 201 { case, harbor }

  Op->>GW: POST /v1/disputes/cases/{id}/notes
  Op->>GW: POST /v1/disputes/cases/{id}/evidence { label, ref_uri }
  GW->>PG: Append timeline / evidence refs

  Op->>GW: POST /v1/disputes/cases/{id}/resolve { resolution }
  alt resolved_release
    GW->>HB: POST .../dispute/resolve { action: release }
    HB->>Rail: Capture / release
    HB-->>HB: released
    GW->>PG: status resolved_release
  else resolved_refund
    GW->>HB: POST .../dispute/resolve { action: refund }
    HB->>Rail: Cancel / refund
    HB-->>HB: refunded
    GW->>PG: status resolved_refund
  else resolved_split or escalated_external
    GW->>PG: Case status only (no Harbor resolve)
  end

Harbor intent state machine (dispute subset)

stateDiagram-v2
  [*] --> funded
  funded --> evidence_submitted: submit evidence
  funded --> disputed: dispute/open
  evidence_submitted --> disputed: dispute/open
  evidence_submitted --> released: confirm + passed
  evidence_submitted --> refunded: confirm + failed
  disputed --> released: dispute/resolve release
  disputed --> refunded: dispute/resolve refund
  released --> [*]
  refunded --> [*]

Terminal states released and refunded are not re-opened by provider webhooks alone. Late Stripe dispute events on ACH may set settlement_exception_* while state remains terminal — see chargeback section below.

Gateway dispute case statuses

Case statusHarbor effectTypical use
openNoneSettlement-only case (settlement_object_ref, no intent_id)
frozenIntent already disputed via combined createActive review
resolved_releaseHarbor released via resolveOperator approves payee after freeze
resolved_refundHarbor refunded via resolveOperator sides with principal
resolved_splitNoneOff-rail split; manual money movement outside Harbor partial capture
escalated_externalNoneExternal arbitration; export packet

Resolve API accepts resolution values: resolved_release, resolved_refund, resolved_split, escalated_external (aliases release / refund also accepted for Harbor-backed resolutions).

POST /v1/disputes/cases/link-disputed-intent verifies Harbor state = disputed, then inserts a case without calling dispute/open again. Returns existing case if one is already registered for the intent.

Implementation: disputes.go.

Stripe chargeback vs Harbor dispute

sequenceDiagram
  participant Stripe
  participant GW as Gateway webhooks
  participant HB as Harbor ingest
  participant Sig as Signal

  Stripe->>GW: charge.dispute.created
  GW->>HB: POST /internal/v1/stripe/events
  HB->>HB: Match intent via metadata.paybond_intent_id
  HB->>HB: settlement_exception manual_review (intent state unchanged)
  HB-->>Sig: PROVIDER_STRIPE_DISPUTE observation (optional)
  Note over HB: Operator must explicitly dispute/open for freeze

ACH ingest sets exception code stripe_ach_charge_dispute (stripe_sync.rs). Signal maps charge.dispute.* to PROVIDER_STRIPE_DISPUTE (fraud_signals.go).

SignalHarbor intent stateGateway caseSettlement frozen
Predicate passed: falseMoves to refunded on confirmOptionalNo — automated refund path
Operator dispute/opendisputedCreated via cases APIYes
Stripe charge.dispute.*UnchangedNot auto-createdNo

HTTP surface

Evidence and settlement (tenant API via Gateway)

MethodPathRole
POST/harbor/intents/{id}/evidenceSubmit signed evidence; predicate evaluation
POST/harbor/intents/{id}/settlement/confirmTerminal release or refund from stored evaluation

Harbor operator (direct or Gateway proxy)

MethodPathBodyEffect
POST/operator/v1/intents/{id}/dispute/open{ summary, reason_code }disputed, settlement frozen
POST/operator/v1/intents/{id}/dispute/resolve{ action: release | refund }Terminal money move from disputed
GET/operator/v1/intents/{id}Timeline, exceptions, provider fields

Gateway proxy prefix: /harbor/operator/v1/intents/{id}/dispute/...

Gateway dispute cases

Requires CanManageDisputes RBAC and Private dashboards commercial feature.

MethodPathRole
POST/v1/disputes/casesOpen case (+ Harbor freeze when intent_id set)
POST/v1/disputes/cases/link-disputed-intentLink case to existing Harbor dispute
GET/v1/disputes/casesList (filters: status, intent_id, pagination)
GET/v1/disputes/cases/{case_id}Detail + timeline + evidence refs
POST/v1/disputes/cases/{case_id}/resolveResolve (Harbor-backed or gateway-only)
POST/v1/disputes/cases/{case_id}/assignAssignee user
POST/v1/disputes/cases/{case_id}/evidenceAttach { label, ref_uri }
POST/v1/disputes/cases/{case_id}/notes{ body, visibility: tenant | internal }
GET/v1/disputes/cases/{case_id}/export/arbitrationArbitration handoff JSON

Schemas: Gateway OpenAPI.

ArbitrationHandoffPacketV1

Stable export shape for third-party reviewers (PAYBOND-V1-005):

{
  "packet_version": "v1",
  "tenant_realm": "550e8400-e29b-41d4-a716-446655440000",
  "case": { },
  "timeline": [
    {
      "seq": 1,
      "event_type": "case_opened",
      "created_at": "2026-07-02T13:00:00Z",
      "payload": { }
    }
  ],
  "evidence_refs": [
    {
      "label": "vendor_ticket",
      "ref_uri": "https://support.example/ticket/123"
    }
  ],
  "content_sha256_hex": "…"
}
FieldDescription
packet_versionAlways v1 today
tenant_realmAuthenticated tenant realm id
caseFull paybond_dispute_case row
timelineNon-internal rows by default; include_internal=1 requires tenant admin or audited support
evidence_refsURI references attached to the case (not Harbor evidence payloads)
content_sha256_hexSHA-256 of canonical JSON with this field empty before final serialization

Defined in ArbitrationHandoffPacketV1.

Decision reference

SituationRecommended path
Evidence clearly fails declared ruleAccept predicate_passed: falsesettlement/confirmrefunded
Evidence passes rule; partner disagreesdispute/open before or instead of confirm; human resolve
Ambiguous fulfillmentDispute freeze; gather case evidence refs; resolve or escalate
Stripe chargeback after releaseReview settlement_exception_*; open dispute only if freeze/action needed
Partial settlement off-railresolved_split (no Harbor partial capture) + manual provider action
External arbitratorescalated_externalGET .../export/arbitration

Kit and Signal boundaries

SurfaceEvidenceDisputes
KitsubmitEvidence, middleware auto-evidenceNot exposed — Gateway HTTP / console only
SignalLifecycle outcomes from Harbor terminal eventsPROVIDER_STRIPE_DISPUTE and disputed outcome counters

See support matrix.