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
| Layer | What it is | Who drives it | Money outcome |
|---|---|---|---|
| Evidence evaluation | Automated predicate run at POST /intents/{id}/evidence | Payee-signed payload and 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 and 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 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 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 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 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; 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_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 proof-gated 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.
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
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
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.