paybondpaybond
Sign in

Disputes and evidence evaluation

How Paybond evaluates signed evidence with predicates, when disputes freeze settlement, and how Stripe vs Adyen chargebacks differ from Harbor disputes.

In short: Paybond checks proof of work against a completion rule automatically, and only asks a human to step in when that isn't enough. Paybond separates automated evidence evaluation from human dispute handling. Confusing the two is a common production mistake: a completion rule (predicate) that returns passed: false is a normal business outcome (refund path), not a transport error. Provider chargebacks are not all the same: a Stripe chargeback webhook does not automatically open a Harbor dispute, while a bound Adyen CHARGEBACK freezes disputable intents (see below).

This guide is for developers building evidence payloads, security reviewers auditing tenant isolation, and operators planning dispute runbooks. For Kit signing details, see Evidence & artifacts. For the intent state machine, see Intent lifecycle.

Two layers — do not conflate them

LayerWhat it isWho drives itMoney outcome
Evidence evaluationAutomated predicate run at POST /intents/{id}/evidencePayee-signed payload and Harbor predicate VMsettlement/confirmreleased or refunded from stored predicate_passed
Dispute handlingHuman-in-the-loop freeze and case workflowOperator opens Harbor dispute and Gateway case APIsHarbor dispute/resolvereleased or refunded while frozen; or Gateway-only statuses for off-rail outcomes

Evidence evaluation answers: did the signed facts satisfy the intent rule?
Dispute handling answers: should we override or pause automation because humans disagree, law requires review, or a provider event needs triage?

Evidence evaluation flow

After an intent is funded, the payee (or Kit middleware on the payee's behalf) submits one evidence binding per intent. Harbor evaluates the completion rule, records the report, and moves the intent to evidence_submitted. Terminal money movement happens only after POST /intents/{id}/settlement/confirm.

For managed completion_budget_v1 / cost_and_completion, the validated signed cost_cents also becomes the server-derived terminal amount on supported rails. The authorized amount remains a ceiling. The confirm caller cannot override the amount, tenant, or destination; routing stays bound to authenticated tenant context and the provider destination snapshotted on the intent.

Payee / Kit Gateway Harbor | | | |-- POST .../evidence ---->|-- proxy (tenant scope) -->| | | | verify payee signature (EvidenceSignV1, version 2) | | | load predicate_dsl or managed policy_binding | | | run predicate VM and optional schema drift report | | | state: funded → evidence_submitted |<-- 200 with evaluation --|<--------------------------| | predicate_passed | | | predicate_evaluation | | | | | |-- POST .../settlement/confirm ----------------------->| | | | drive_terminal_settlement(passed) | | | released OR refunded

Inputs Harbor evaluates

  • Inline predicatepredicate_dsl on the intent (signing v6).
  • Managed policypolicy_binding referencing a published policy head (signing v7).
  • Completion preset — optional completion_preset_id from the completion presets catalog; frozen on the intent at create time.
  • Payee signatureEvidenceSignV1 with version = 2, bincode 2 standard encoding. Kit builders live in kit/ts/src/agent/evidence.ts and the Python parity module.

Ledger and audit events

Harbor appends provenance events including EvidenceSubmitted, PredicateEvaluated, and CompletionSchemaValidated (when a preset is bound). The evaluation report is returned in the evidence response body and stored on the intent row.

Predicate failure is a business outcome

When the predicate does not pass, Harbor still returns HTTP 200 with predicate_passed: false and a structured predicate_evaluation report. That is not a 4xx validation error. The integrator (or Kit middleware) then calls settlement/confirm, and Harbor refunds or cancels through the active rail.

Treat passed: false like a declined settlement rule, not a retryable transport failure — unless you are correcting the evidence payload itself before first submission.

Schema drift is signal-only (v1)

When a completion preset is bound, Harbor runs completion-schema validation at submit time (signal-only in v1). Vendor or quality drift is reported in schema_validation but does not block submission in v1. A passing predicate with drift may include passed_with_drift in drift_kinds for operator review.

Evidence binds once

Harbor accepts evidence only from fundedevidence_submitted. There is no supported re-submission path to replace evidence on the same intent. If partners disagree after submission, use the dispute workflow rather than attempting a second evidence POST.

Dispute handling flow

Disputes freeze automated settlement. Open them when human judgment is required before or instead of accepting the predicate outcome.

Operator / Console Gateway Harbor | | | |-- POST /v1/disputes/cases| | | { intent_id, ... } |-- POST .../dispute/open ----->| | | | state → disputed (settlement frozen) | |<- 2xx ------------------------| | | insert case (status: frozen) | |<-- 201 case and harbor -| | | | | |-- POST .../notes | timeline and evidence refs | |-- POST .../evidence | case evidence attachments | | | | |-- POST .../resolve | | | resolution: |-- POST .../dispute/resolve -->| | resolved_release | { action: release } | capture / release rail | resolved_refund | { action: refund } | cancel / refund rail |<-- 200 ------------------| |

Opening a case

POST /v1/disputes/cases with intent_id is the combined path: Gateway calls Harbor POST /operator/v1/intents/{id}/dispute/open, then persists a tenant-scoped case row with status frozen.

If Harbor is already disputed (for example the console opened Harbor first), use POST /v1/disputes/cases/link-disputed-intent to attach a case row without calling Harbor open again.

Settlement-only cases (no intent) are supported with settlement_object_ref and status open; they do not call Harbor.

Resolving a case

Gateway resolutionHarbor callIntent terminal stateRail money move
resolved_releasedispute/resolve with action: releasereleasedCapture / transfer per rail
resolved_refunddispute/resolve with action: refundrefundedCancel / refund per rail
resolved_splitNoneUnchangedOff-rail; case status only
escalated_externalNoneUnchangedExternal arbitration handoff

resolved_split and escalated_external update Gateway case workflow only; they do not ask Harbor to calculate an arbitrary split. This is separate from automated variable-cost settlement: managed completion_budget_v1 can partially capture or transfer the validated evidence cost on supported rails, but dispute resolution does not accept a caller-supplied partial amount.

Dispute APIs require the Private dashboards commercial feature (Team plan and above in the public catalog).

Stripe chargeback ≠ automatic Paybond dispute

Stripe charge.dispute.* webhooks are provider signals, not Harbor state transitions.

When Harbor ingests a bound Stripe dispute event (for example on ACH rails), it may set:

  • settlement_exception_category = manual_review
  • settlement_exception_code = stripe_ach_charge_dispute
  • stripe_reconciliation_status = stripe_ahead

The intent state is unchanged — including if the intent was already released. Signal may emit a PROVIDER_STRIPE_DISPUTE fraud-signal counter for operator dashboards; that does not freeze escrow or open a Gateway case.

Operator checklist when Stripe reports a dispute

  1. Locate the intent via metadata.paybond_intent_id on the Stripe PaymentIntent or charge.
  2. Compare proof-gated lifecycle (GET /operator/v1/intents/{id}) with Stripe Dashboard status.
  3. Review existing predicate evaluation and evidence — was settlement already terminal?
  4. Explicitly open a Harbor dispute if you need to freeze automation or drive a resolve action:
    • POST /v1/disputes/cases with intent_id, or
    • POST /harbor/operator/v1/intents/{id}/dispute/open then link the case.
  5. Attach external documents with POST /v1/disputes/cases/{case_id}/evidence (URI references, not Harbor evidence re-submit).
  6. Resolve with resolved_release or resolved_refund when the human decision is final; use escalated_external before exporting the arbitration packet.

For the full machine-readable exception code catalog (pi_canceled_while_active, stripe_ach_charge_dispute, stale_exception_resolved, and others), see Settlement with payment providers.

Adyen chargeback → dispute freeze (bound only)

Unlike Stripe, Paybond does auto-freeze on a bound Adyen CHARGEBACK notification:

  • Binding requires matching tenant, intent, and authorization payment reference.
  • For disputable states (funded / evidence_submitted), automated settlement stops until an operator resolves the dispute.
  • Incomplete binding (for example no stored auth payment reference) and non-disputable states stay observation / manual review — no auto-freeze.

Enable CHARGEBACK on the Adyen Customer Area webhook when you want this path. Destination HMAC remains required for live ingest. Setup: Configure Adyen settlement.

Decision tree — predicate vs dispute

Evidence submitted? ├─ No → fund / tool / wait path (not dispute) └─ Yes → predicate_passed? ├─ true → Accept auto-release? │ ├─ Yes → settlement/confirm → released │ └─ No (partner disagrees, ambiguous fulfillment) → open dispute BEFORE confirm ├─ false → Accept auto-refund? │ ├─ Yes → settlement/confirm → refunded │ └─ No (payee contests, partial delivery) → open dispute BEFORE confirm if still evidence_submitted; │ if already refunded, open case for off-rail recovery (resolved_split / external) ├─ Stripe charge.dispute.* webhook? │ └─ Treat as manual_review signal → operator checklist (does NOT auto-open dispute) └─ Adyen CHARGEBACK webhook? ├─ Bound (tenant / intent / payment reference) on funded|evidence_submitted → freeze settlement └─ Unbound / incomplete → observation / manual review only

Prefer accepting predicate outcomes when signed evidence clearly matches the declared rule and both parties agreed to that rule at intent create time.

Open a dispute when:

  • Partner rejects fulfillment but evidence structurally passes (policy disagreement).
  • Evidence is ambiguous and you must not auto-refund or auto-release.
  • Legal, contractual, or regulatory review is required.
  • Stripe reports a chargeback or settlement exception and you need a frozen record before rail action.
  • An unbound Adyen chargeback needs operator review (bound Adyen chargebacks already freeze disputable intents).

Gateway case API examples

Set environment variables from your tenant console or paybond login. The x-tenant-id header must match the JWT tenant realm when auth is enforced.

Terminal
Terminal commandSwipe to inspect long lines
export PAYBOND_GATEWAY_URL="https://api.paybond.example"
export PAYBOND_TENANT_ID="550e8400-e29b-41d4-a716-446655440000"
export PAYBOND_ACCESS_TOKEN="<operator-or-tenant-admin-bearer>"
export INTENT_ID="8c9e6679-7425-40de-944b-e07fc1f90ae7"

Create a case (Harbor freeze and Gateway row)

Terminal
Terminal commandSwipe to inspect long lines
curl -sS -X POST "$PAYBOND_GATEWAY_URL/v1/disputes/cases" \
  -H "Authorization: Bearer $PAYBOND_ACCESS_TOKEN" \
  -H "x-tenant-id: $PAYBOND_TENANT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "intent_id": "'"$INTENT_ID"'",
    "subject": "Partner disputes API delivery evidence",
    "harbor_summary": "Payee claims partial outage; predicate passed on HTTP 200",
    "reason_code": "partner_disagreement",
    "settlement_object_ref": "pi_example"
  }'
Terminal
Terminal commandSwipe to inspect long lines
curl -sS -X POST "$PAYBOND_GATEWAY_URL/v1/disputes/cases/link-disputed-intent" \
  -H "Authorization: Bearer $PAYBOND_ACCESS_TOKEN" \
  -H "x-tenant-id: $PAYBOND_TENANT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "intent_id": "'"$INTENT_ID"'",
    "subject": "Console-opened Harbor dispute",
    "reason_code": "operator_review"
  }'

Add an internal note

Terminal
Terminal commandSwipe to inspect long lines
export CASE_ID="9c9e6679-7425-40de-944b-e07fc1f90ae7"

curl -sS -X POST "$PAYBOND_GATEWAY_URL/v1/disputes/cases/$CASE_ID/notes" \
  -H "Authorization: Bearer $PAYBOND_ACCESS_TOKEN" \
  -H "x-tenant-id: $PAYBOND_TENANT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "body": "Requested vendor logs; holding release until review completes.",
    "visibility": "internal"
  }'

Internal notes require tenant admin or an audited support session.

Resolve to release (Harbor capture)

Terminal
Terminal commandSwipe to inspect long lines
curl -sS -X POST "$PAYBOND_GATEWAY_URL/v1/disputes/cases/$CASE_ID/resolve" \
  -H "Authorization: Bearer $PAYBOND_ACCESS_TOKEN" \
  -H "x-tenant-id: $PAYBOND_TENANT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "resolution": "resolved_release",
    "notes": "Evidence supports release after manual review."
  }'

Export arbitration packet

Terminal
Terminal commandSwipe to inspect long lines
curl -sS "$PAYBOND_GATEWAY_URL/v1/disputes/cases/$CASE_ID/export/arbitration" \
  -H "Authorization: Bearer $PAYBOND_ACCESS_TOKEN" \
  -H "x-tenant-id: $PAYBOND_TENANT_ID" \
  -o arbitration-packet.json

The export returns ArbitrationHandoffPacketV1 with packet_version, tenant_realm, case row, non-internal timeline, evidence refs, and content_sha256_hex. Rate limited per tenant.

Full request and response schemas: Gateway OpenAPI (Disputes tag).

Kit boundary

Paybond Kit covers evidence submission and middleware auto-evidence after guarded tool calls. There is no paybond.disputes.* SDK surface — disputes are operator console and Gateway HTTP only. See the support matrix.

Signal exposes dispute-related counters (PROVIDER_STRIPE_DISPUTE, lifecycle outcomes) for standing review; it does not replace the case workflow.

Where to go next