paybondpaybond
Sign in

Disputes and evidence evaluation

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

Paybond separates automated evidence evaluation from human dispute handling. Confusing the two is a common production mistake: a predicate that returns passed: false is a normal business outcome (refund path), not a transport error. A Stripe chargeback webhook does not automatically open a Harbor dispute.

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.

Tenant isolation

Every Gateway and Harbor call must carry tenant scope from authenticated credentials (service-account API key, operator session, or audited support session). Dispute cases, timeline rows, and arbitration exports are always filtered by the authenticated tenant. Never pass a tenant id from unauthenticated client input.

Two layers — do not conflate them

LayerWhat it isWho drives itMoney outcome
Evidence evaluationAutomated predicate run at POST /intents/{id}/evidencePayee-signed payload + Harbor predicate VMsettlement/confirmreleased or refunded from stored predicate_passed
Dispute handlingHuman-in-the-loop freeze and case workflowOperator opens Harbor dispute + 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 predicate, records the report, and moves the intent to evidence_submitted. Terminal money movement happens only after POST /intents/{id}/settlement/confirm.

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 + optional schema drift report | | | state: funded → evidence_submitted |<-- 200 + 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 + harbor ----| | | | | |-- POST .../notes | timeline + evidence refs | |-- POST .../evidence | (Gateway Postgres only) | | | | |-- 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. Harbor does not perform partial capture today.

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 Harbor 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.

Full triage SLAs and RBAC: Operations runbook §2.

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)

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.

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 + 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