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
| Layer | What it is | Who drives it | Money outcome |
|---|---|---|---|
| Evidence evaluation | Automated predicate run at POST /intents/{id}/evidence | Payee-signed payload + Harbor predicate VM | settlement/confirm → released or refunded from stored predicate_passed |
| Dispute handling | Human-in-the-loop freeze and case workflow | Operator opens Harbor dispute + Gateway case APIs | Harbor dispute/resolve → released 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 predicate —
predicate_dslon the intent (signing v6). - Managed policy —
policy_bindingreferencing a published policy head (signing v7). - Completion preset — optional
completion_preset_idfrom the completion presets catalog; frozen on the intent at create time. - Payee signature —
EvidenceSignV1withversion = 2, bincode 2 standard encoding. Kit builders live inkit/ts/src/agent/evidence.tsand 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 funded → evidence_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 resolution | Harbor call | Intent terminal state | Rail money move |
|---|---|---|---|
resolved_release | dispute/resolve with action: release | released | Capture / transfer per rail |
resolved_refund | dispute/resolve with action: refund | refunded | Cancel / refund per rail |
resolved_split | None | Unchanged | Off-rail; case status only |
escalated_external | None | Unchanged | External 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_reviewsettlement_exception_code=stripe_ach_charge_disputestripe_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
- Locate the intent via
metadata.paybond_intent_idon the Stripe PaymentIntent or charge. - Compare Harbor lifecycle (
GET /operator/v1/intents/{id}) with Stripe Dashboard status. - Review existing predicate evaluation and evidence — was settlement already terminal?
- Explicitly open a Harbor dispute if you need to freeze automation or drive a resolve action:
POST /v1/disputes/caseswithintent_id, orPOST /harbor/operator/v1/intents/{id}/dispute/openthen link the case.
- Attach external documents with
POST /v1/disputes/cases/{case_id}/evidence(URI references, not Harbor evidence re-submit). - Resolve with
resolved_releaseorresolved_refundwhen the human decision is final; useescalated_externalbefore 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
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
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"
}'Link a case after Harbor is already disputed
terminal
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
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
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
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.jsonThe 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.