How to Integrate Salesforce and Zendesk Without Breaking Ownership and Reporting
A trustworthy Salesforce Zendesk integration starts with an audit, not a sync job. Run g-gremlin zendesk drift audit to surface 12 categorized identity-drift issues, review the grouped exceptions_by_account.csv, and apply only 4 safe Zendesk-side repairs under a matching --confirm-plan-hash. Every run produces a fix_plan.json and an apply_receipt.json.
Published April 3, 2026 - Updated April 17, 2026 - Based on real Salesforce and Zendesk drift work
This guide is grounded in real Salesforce and Zendesk integration work. Client names, object names, and environment-specific details have been removed; the reusable contract and the CLI surface are the parts that matter.
Short answer
Do not integrate Salesforce and Zendesk blindly. Audit first.
- Salesforce stays authoritative for account identity, ownership, and stage. Zendesk receives a linked service object.
- The canonical link is external_id on the Zendesk org equal to the Salesforce account id. Domain-based matching is a secondary review signal.
- Run g-gremlin zendesk drift audit to detect drift across 12 issue codes and get grouped, account-level exceptions.
- Apply only 4 safe Zendesk-side actions: set_org_external_id, add_org_domain, assign_user_to_org, move_user_to_org. Live apply requires --apply and --confirm-plan-hash.
- Ticket issues are detected but never trigger writes. Salesforce writes are never performed by this tool.
What people mean by Salesforce Zendesk integration
The same search phrase can mean a UI connector, an iPaaS workflow, a Salesforce-native service, or an audit-first identity contract. Start by separating the query from the system you actually need.
Search phrase
zendesk salesforce integration
Practical answer
Treat Salesforce as the customer-identity source of truth and Zendesk as the support-side customer object. The durable link is Zendesk organization external_id mapped to Salesforce Account Id.
Integration route
Start with an audit-first integration contract before installing or building a sync.
Search phrase
salesforce zendesk sync
Practical answer
A sync is only safe when ownership, lifecycle, and reporting fields have one authority. For most B2B teams, Salesforce owns those fields and Zendesk receives a linked org plus narrow support context.
Integration route
Audit orgs, domains, users, and tickets before deciding what should sync continuously.
Search phrase
does Zendesk integrate with Salesforce?
Practical answer
Yes, but the hard part is not whether an API connection exists. The hard part is preventing duplicate orgs, bad external_ids, domain collisions, and reporting drift after the first integration works.
Integration route
Use the UI or an iPaaS for simple paths; use CLI audits and receipts when identity drift matters.
Search phrase
Zendesk Salesforce without Workato or MuleSoft
Practical answer
A focused create-or-link org integration can be thinner than a full iPaaS project if the contract is explicit and the audit layer catches drift.
Integration route
Buy a platform when you need broad multi-system orchestration. Build thinner when the scope is account identity and service linkage.
Try the Zendesk Salesforce integration audit from your terminal
g-gremlin zendesk drift audit --demo loads bundled fixture data, runs the full drift audit, and writes the same artifact set as a live run. No Zendesk API token, no Salesforce connection, no live CRM data required.
terminal
$ g-gremlin zendesk drift audit --demo
Loading fixture data...
9 Salesforce accounts, 11 Zendesk orgs, 24 users, 142 memberships, 487 tickets (30d lookback)
Resolving identities...
7 accounts uniquely resolved to Zendesk orgs
1 account blocked: duplicate external_id (2 orgs share sf_account_id 001ACME002)
1 account missing Zendesk org entirely
Detecting drift...
Org issues: 5 (2 missing external_id, 1 missing expected domain, 1 domain attached elsewhere, 1 duplicate external_id)
User issues: 4 (2 missing expected membership, 1 default org not expected, 1 domain ambiguous)
Ticket issues: 3 (2 org mismatch, 1 resolution blocked)
Preparing fix plan...
Prepared (safe-to-apply): 5 actions
set_org_external_id: 2
add_org_domain: 1
assign_user_to_org: 2
Blocked (review-only): 4 actions
top reasons: duplicate_external_id (2), domain_belongs_to_other_org (1), user_domain_ambiguous (1)
Artifacts written to:
./artifacts/zendesk_drift/20260417_221530Z_audit/
summary.md, summary.json, org_issues.csv, user_issues.csv,
ticket_issues.csv, all_exceptions.csv, exceptions_by_account.csv,
fix_plan.csv, fix_plan.json, receipt.json, apply_receipt.json
plan_hash: 8c5d4d4d4b0f1a7e2c9f3b6d5a8e4f1c2a9b3e8d7f5c4a2b6e9d1f3c8a5b7e4d
Next:
Dry-run apply:
g-gremlin zendesk drift apply \
--plan ./artifacts/zendesk_drift/20260417_221530Z_audit/fix_plan.json
Trace one account:
g-gremlin zendesk drift trace --sf-account-id 001ACME001 --demoDemo mode
No auth, no network. Demo plans are dry-run only, so the suggested next step is a dry-run apply or a trace command, never a live apply.
Deterministic
Same fixtures, same plan_hash, same grouped exceptions every run. Useful for screenshots, docs, and CI smoke tests.
Real shape
Demo output is the same format as live audits: summary.md, exceptions_by_account.csv, fix_plan.json with a plan_hash, and an apply_receipt.json.
What the audit produces
Every audit run writes one folder under ./artifacts/zendesk_drift/ with a fixed set of operator artifacts. exceptions_by_account.csv and fix_plan.json are the two files you will open first.
summary.md
Operator-facing narrative: issue counts, blockers, and the recommended next command.
summary.json
Structured counts, plan_hash, and run metadata for automation or CI.
org_issues.csv
One row per org drift issue with issue_code, risk, current and expected external_id, and missing domains.
user_issues.csv
One row per user drift issue with membership state, expected org, and suppression reason.
ticket_issues.csv
One row per ticket attribution issue. Ticket issues never generate write actions.
all_exceptions.csv
Flat union of org, user, and ticket issues with scope, issue_code, risk, and status.
exceptions_by_account.csv
Grouped triage file, one row per Salesforce account. The file operators actually open first.
fix_plan.csv
Flat view of every prepared and blocked action with preconditions and blocked_reason.
fix_plan.json
Canonical machine-readable plan with a stable plan_hash. Required input to apply.
receipt.json
Audit run receipt: command metadata, counts, warnings, blockers, and plan_hash.
apply_receipt.json
Initialized with mode: not_run during audit. Populated on dry-run and live apply with per-action results.
exceptions_by_account.csv: the grouped triage file
One row per Salesforce account. This is the file the support-ops or RevOps team opens in a spreadsheet to decide which accounts are safe to apply, which are blocked, and why.
| sf_account_id | account_name | org | user | ticket | total | prepared | blocked | top_issue_codes | top_blocked_reasons |
|---|---|---|---|---|---|---|---|---|---|
| 001ACME001 | Acme Healthcare | 1 | 0 | 0 | 1 | 1 | 0 | org_missing_external_id | |
| 001ACME002 | Acme Labs | 1 | 1 | 1 | 3 | 0 | 2 | org_duplicate_external_id, user_resolution_blocked_duplicate_external_id | duplicate_external_id |
| 001ACME003 | Acme Biotech | 1 | 0 | 0 | 1 | 0 | 1 | org_domain_attached_elsewhere | domain_belongs_to_other_org |
| 001ACME004 | Acme Retail | 1 | 1 | 0 | 2 | 2 | 0 | org_missing_expected_domain, user_missing_expected_membership | |
| 001ACME005 | Acme Logistics | 0 | 1 | 0 | 1 | 0 | 1 | user_domain_ambiguous | user_domain_ambiguous |
| 001ACME006 | Acme Studios | 0 | 0 | 2 | 2 | 0 | 0 | ticket_org_mismatch |
Safe-to-apply rows
Nonzero prepared_action_count with blocked_action_count at zero. Pass the fix plan to apply with a matching --confirm-plan-hash.
Blocked rows
Duplicate external_id, domain_belongs_to_other_org, or user_domain_ambiguous. Resolve in Zendesk, then re-run the audit.
Ticket-only rows
Ticket issues are detected but never generate write actions. They signal that upstream org or user drift is affecting routing.
Anonymized. Account ids and names are illustrative, not customer data.
The 12 Zendesk and Salesforce drift issue codes
Every issue the audit can emit is enumerated here: 6 org, 4 user, 2 ticket. The ticket codes are detection only and never generate write actions.
Org (6)
org_duplicate_external_id
Two or more Zendesk orgs share the same external_id. Blocks every downstream user and ticket decision for that account.
org_missing_external_id
A unique domain-based org candidate exists and its external_id is blank. Safe-to-apply via set_org_external_id.
org_external_id_conflict
A likely org candidate has a nonblank external_id that conflicts with the expected Salesforce account. Review-only.
org_missing_expected_domain
The resolved org is missing one or more expected domains. Safe-to-apply via add_org_domain after collision precheck.
org_domain_attached_elsewhere
An expected domain already belongs to another Zendesk org. Blocked with reason domain_belongs_to_other_org.
account_missing_zendesk_org
The Salesforce account has no unique Zendesk org match and no safe domain-resolved target. Review-only.
User (4)
user_missing_expected_membership
A user maps deterministically to an account but is not a member of the expected org. Safe-to-apply via assign_user_to_org.
user_default_org_not_expected
User is already a member of the expected org, but the default org differs. Safe-to-apply via move_user_to_org.
user_resolution_blocked_duplicate_external_id
The user depends on an account/org mapping that is blocked by duplicate external_ids upstream. Review-only.
user_domain_ambiguous
A non-personal email domain maps to multiple candidate accounts or orgs. Review-only. Personal-domain users are suppressed entirely.
Ticket (2) - detection only
ticket_org_mismatch
A ticket organization_id does not match the requester’s deterministically resolved expected org. Detection only, no write.
ticket_resolution_blocked_duplicate_external_id
Ticket attribution cannot be trusted because the requester’s account resolution is blocked upstream. Detection only.
Deep dives on the top issue codes
- How to fix duplicate external_id in Zendesk - the critical blocker for org_duplicate_external_id.
- How to link a Zendesk organization to a Salesforce account - the safe-to-apply path for org_missing_external_id and the manual path for account_missing_zendesk_org.
- Zendesk domain collision between organizations - what to do when org_domain_attached_elsewhere blocks add_org_domain with domain_belongs_to_other_org.
- Fix the wrong default organization for a Zendesk user - the safe-to-apply fix for user_default_org_not_expected via move_user_to_org.
- Zendesk drift issue code reference - every issue code, safe action, precondition, and blocked_reason in one glossary.
What g-gremlin zendesk drift apply will do - and will not do
The apply surface is deliberately narrow. 4 safe actions, Zendesk-side only, plan-hash protected, capped, and re-validated at apply time.
Will do: 4 safe actions
Zendesk-side only. Every action re-validates preconditions at apply time.
set_org_external_id
Set a blank Zendesk org external_id to the Salesforce account id when exactly one target org is resolved and no other org already uses the id.
add_org_domain
Append one missing expected domain to the resolved org. Precheck blocks if the domain belongs to another org or if the account is ambiguous.
assign_user_to_org
Add a missing organization membership for a deterministically resolved user. Does not change the default org.
move_user_to_org
Make an existing target membership the user’s default org. Does not add or remove membership.
Will not do
Hard-blocked by design. These are non-negotiable scope boundaries.
- No Zendesk ticket mutation of any kind.
- No Zendesk org create, merge, delete, or bulk restructure.
- No Salesforce writes, ever. Salesforce stays read-only reference input.
- No live apply without --apply and a matching --confirm-plan-hash.
- No action beyond --max-actions (default 25) or --max-per-account (default 3).
- No action on accounts blocked by duplicate external_id, and no fuzzy or LLM-driven matching in the apply path.
The apply contract: plan hash, caps, receipts
Live apply requires a matching --confirm-plan-hash plus --apply. Caps are enforced before any write. Every attempted, applied, blocked, skipped, and failed action lands in apply_receipt.json.
Live apply command
Demo plans are dry-run only. This form is for a live audit plan.
g-gremlin zendesk drift apply \
--plan ./artifacts/zendesk_drift/20260417_221530Z_audit/fix_plan.json \
--apply \
--confirm-plan-hash 8c5d4d4d4b0f1a7e2c9f3b6d5a8e4f1c2a9b3e8d7f5c4a2b6e9d1f3c8a5b7e4d \
--max-actions 25 \
--max-per-account 3apply_receipt.json excerpt
Four actions applied, one skipped at apply time because a precondition changed since the audit. No action is silently dropped.
{
"run_id": "20260417_223045Z_apply",
"source_plan_path": "./artifacts/zendesk_drift/20260417_221530Z_audit/fix_plan.json",
"source_plan_hash": "8c5d4d4d4b0f1a7e2c9f3b6d5a8e4f1c2a9b3e8d7f5c4a2b6e9d1f3c8a5b7e4d",
"mode": "apply",
"caps_used": { "max_actions": 25, "max_per_account": 3 },
"counts": { "attempted": 5, "applied": 4, "blocked": 0, "skipped": 1, "failed": 0 },
"results": [
{ "action_id": "A001", "action_code": "set_org_external_id", "status": "applied", "sf_account_id": "001ACME001" },
{ "action_id": "A002", "action_code": "set_org_external_id", "status": "applied", "sf_account_id": "001ACME007" },
{ "action_id": "A003", "action_code": "add_org_domain", "status": "applied", "sf_account_id": "001ACME004" },
{ "action_id": "A004", "action_code": "assign_user_to_org", "status": "applied", "sf_account_id": "001ACME004" },
{ "action_id": "A005", "action_code": "assign_user_to_org", "status": "skipped",
"reason": "precondition_changed_user_already_member", "sf_account_id": "001ACME008" }
]
}Caps enforced up front
--max-actions 25 and --max-per-account 3 by default.
Plan-hash protected
--confirm-plan-hash must match fix_plan.json.plan_hash.
Preconditions re-checked
Stale rows skipped with an explicit reason in the receipt.
Salesforce stays authoritative, Zendesk stays linked
The integration architecture that holds up under drift audits. One system owns customer truth. The other receives a linked, explicit, auditable org.
Salesforce owns the customer record
Account ownership, customer stage, and reporting stay authoritative in Salesforce. Zendesk receives a linked service object, not the right to redefine CRM truth.
external_id is the canonical link
A unique Zendesk org external_id equal to the Salesforce account id is how Gremlin uniquely resolves an account. Every other lookup is a secondary or review-only signal.
Audit-first, not sync-first
Before any writeback job runs, prove what is drifted. g-gremlin zendesk drift audit produces grouped, account-level exceptions instead of silent sync failures.
Bounded, hashed apply
Only 4 safe Zendesk-side actions ever run. Every live apply requires a matching --confirm-plan-hash and obeys --max-actions and --max-per-account caps.
Receipts, not green checkmarks
apply_receipt.json records every attempted, applied, blocked, skipped, and failed action. A green API response is not proof that the customer graph is clean.
Matching criteria is the critical control point
Salesforce Zendesk integrations fail for identity reasons before they fail for API reasons. The audit resolves accounts in one strict order, and the rest of the integration inherits that discipline.
- sf_account_id is the canonical Salesforce account key.
- A Zendesk org is uniquely resolved to an account when exactly one org has external_id == sf_account_id.
- If more than one Zendesk org shares the same external_id, the account is ambiguous and every downstream user and ticket decision is review-only.
- If no unique external_id match exists, exact domain overlap can identify review candidates. Fuzzy domain inference is never used.
- Personal email domains (gmail.com, outlook.com, yahoo.com, icloud.com, hotmail.com, protonmail.com, and similar) are suppressed from user drift detection entirely.
Failure modes to avoid
These are the patterns that quietly poison Salesforce Zendesk integrations.
Bidirectional ownership fields
If both systems can change owner, lifecycle stage, or reporting-critical fields, nobody can explain why the customer record changed.
Create-on-every-trigger behavior
Without an idempotent create-or-link contract, a single customer can spawn multiple Zendesk orgs as records move through the sales process.
Matching on mutable fields
If Account Name becomes the primary key, a rename, acquisition, or support-side edit can break the relationship while the sync still reports success.
Callouts inside hot transaction paths
Zendesk API work inside synchronous triggers creates brittle failures, poor user experience, and no reliable retry story.
No domain normalization or collision handling
Support systems get messy fast when domains are blank, inconsistent, or shared across multiple account records.
No audit layer before apply
The integration may look fine on day one while duplicate external_ids, domain collisions, and wrong default orgs silently pile up.
Implementation checklist
The rollout sequence that keeps ownership, reporting, and Zendesk attachment clean.
Publish the system-of-record contract
Decide which Salesforce fields stay authoritative, which Zendesk fields can mirror, and that external_id on the Zendesk org is the canonical link to the Salesforce account id.
Run g-gremlin zendesk drift init
Generate gremlin.zendesk_drift.yaml, validate the Salesforce reference CSV headers (sf_account_id, account_name, account_domains, optional website and zendesk_domain_override), and create the output root.
Run g-gremlin zendesk drift audit and review exceptions_by_account.csv
The audit reads orgs, users, organization memberships, and a bounded ticket window, detects drift across 12 issue codes, and writes grouped, account-level exceptions. Triage starts from exceptions_by_account.csv, not summary.md.
Apply safe repairs with --plan, --apply, and --confirm-plan-hash
Dry-run the prepared plan first, then re-run with --apply and a matching --confirm-plan-hash. Caps are enforced before any write. Only the 4 safe actions ever run live.
Wire the audit into a weekly cadence or CI
Re-run g-gremlin zendesk drift audit on a schedule. Proof of value is the reduction in total_issue_count and blocked_action_count over time, not a green sync status.
Publish the operator runbook
Document the flow from audit to review to apply for the support-ops and RevOps teams. A drift-specific playbook and a full issue-code reference are both planned follow-ons to this pillar.
See the Zendesk drift playbook for the full operator execution log, or the issue code reference for every issue code, safe action, and blocked_reason in one place.
Related Salesforce and Zendesk pages
This guide defines the integration contract and the CLI surface. These pages cover the surrounding Salesforce orchestration, Zendesk drift, and comparison decisions.
Zendesk drift playbook
The full operator execution log: Slack message, prompt, audit, grouped exception review, dry-run, and live apply with --confirm-plan-hash.
Open pageAudit Zendesk and Salesforce sync from the CLI
A focused CLI walkthrough of org, user, and ticket audits with trace-user diagnostics.
Open pageKeep Zendesk and Salesforce from drifting apart
The operating model for stable external keys, nightly reconciliation, and a small exception queue.
Open pageIntegrate Salesforce and Zendesk without an integration platform
Native Zendesk sync vs iPaaS vs audit-first CLI, compared side by side with a decision matrix.
Open pageZendesk native sync vs audit-first
A decision guide for when the native Zendesk app is enough and when audit-first is safer.
Open pageSalesforce CLI orchestration
The broader Salesforce operating surface for snapshots, metadata plans, apply receipts, and governed automation.
Open pageSalesforce MCP page
How AI assistants read and plan against Salesforce safely before anything mutates.
Open pageGremlin CLI
The full CLI surface for Salesforce, HubSpot, and Zendesk drift audits.
Open pageFAQ
Answers to the questions buyers and operators actually ask about Salesforce and Zendesk integration.
Does Zendesk integrate with Salesforce?
Yes. The important question is not whether an API connection exists; it is which system owns customer identity, how Zendesk organizations map back to Salesforce Accounts, and how drift is audited after launch. For most B2B teams, Salesforce owns account identity and reporting while Zendesk receives a linked organization through external_id.
How do I audit Zendesk organization drift?
Run g-gremlin zendesk drift audit against a Salesforce reference CSV and your Zendesk credentials. The CLI reads orgs, users, organization memberships, and a bounded ticket window, detects drift across 12 issue codes, and writes grouped results into exceptions_by_account.csv plus a hashed fix_plan.json. Run g-gremlin zendesk drift audit --demo to see the full artifact set against bundled fixtures with no credentials required.
What is g-gremlin zendesk drift?
g-gremlin zendesk drift is a four-command starter package inside Gremlin CLI: init, audit, trace, and apply. It audits Zendesk and Salesforce customer identity drift, produces deterministic grouped exceptions, prepares a hashed fix plan, and safely applies a narrow set of Zendesk-side repairs. It is not an iPaaS, not a sync tool, and it never writes to Salesforce.
How do I fix a duplicate external_id in Zendesk?
Duplicate external_ids are surfaced as org_duplicate_external_id and are deliberately kept review-only. Gremlin blocks every safe action on accounts with duplicate external_ids because downstream user and ticket resolution cannot be trusted. Resolve the ambiguity in Zendesk (pick the surviving org, clear or reassign the external_id on the other) and re-run the audit.
Does the CLI write to Salesforce?
No. g-gremlin zendesk drift never writes to Salesforce. Salesforce is read-only reference input. The four safe actions set_org_external_id, add_org_domain, assign_user_to_org, and move_user_to_org are Zendesk-side only, and the tool explicitly blocks ticket mutations and org create/merge/delete.
Can I run the audit without connecting to my Zendesk?
Yes. g-gremlin zendesk drift audit --demo runs the full audit flow against bundled fixture data, produces the full artifact set including a believable fix_plan.json and apply_receipt.json, and requires no credentials. It is designed for onboarding, screenshots, and previewing the operator workflow before connecting a live Zendesk.
Which system should own customer identity and reporting?
If sales ownership, stage, and reporting live in Salesforce, Salesforce should stay authoritative. Zendesk should receive a linked customer object with a canonical external_id and a reviewed domain set, not become a second source of truth for CRM state.
Should the integration be bidirectional?
Usually not for ownership and reporting fields. Focus on a narrow contract: Salesforce decides who the customer is, Zendesk gets the linked org, and only clearly scoped attributes flow back. This pattern pairs well with an audit-first workflow rather than a continuous bidirectional sync.
How do you prevent duplicate Zendesk organizations?
Use an idempotent create-or-link flow anchored on external_id: stored org id first, external_id second, normalized domain third. Audit continuously with g-gremlin zendesk drift audit so org_duplicate_external_id and org_domain_attached_elsewhere surface as blocked actions before they compound.
What should the matching criteria be?
Treat matching criteria as the critical control point. A unique Zendesk org external_id equal to the Salesforce account id is the canonical match. Domain-based matching is a secondary review signal, and personal email domains such as gmail.com, outlook.com, yahoo.com, icloud.com, hotmail.com, and protonmail.com are excluded from user drift detection entirely.
Why is external_id safer than Account Name?
Account names change. Subsidiaries get renamed. Support teams make manual edits. A stable external_id written into the Zendesk org survives those changes and gives you a deterministic relink path that reporting can trust. It is also the field Gremlin uses for unique account-to-org resolution.
What should happen in Flow versus Apex or a service layer?
Use Flow or trigger logic to decide when an account is eligible to sync. Use an async service for the actual Zendesk API work, retries, and writeback of linkage fields. Keep the drift audit as a separate, scheduled CLI job so detection and mutation never run inside a trigger transaction.
Do I need a full integration platform for this?
Not always. For a focused Salesforce Zendesk organization sync, an audit-first CLI plus a thin, well-audited writeback job can be simpler and safer than buying a larger platform before you need one. Reach for an iPaaS when many systems, many objects, and many business owners all need to orchestrate changes together.
Can I do Salesforce and Zendesk integration without Workato or MuleSoft?
Yes, when the scope is narrow and explicit. If the job is create-or-link organization sync, ownership-safe writeback, retries, and an audit layer that surfaces drift as small reviewable exceptions, a Salesforce-native or thin-service integration plus g-gremlin zendesk drift audit can be enough.
How do I audit Salesforce and Zendesk drift after launch?
Treat audit as part of the integration, not a cleanup project. Schedule g-gremlin zendesk drift audit weekly or wire it into CI. Review exceptions_by_account.csv, resolve any accounts with nonzero blocked_action_count, and apply the prepared safe actions with --confirm-plan-hash. Total issue counts should trend to zero.
How do I keep exceptions reviewable instead of letting the sync hide them?
exceptions_by_account.csv is designed exactly for this. Every account row carries total_issue_count, prepared_action_count, blocked_action_count, top_issue_codes, and top_blocked_reasons. Zero prepared actions on an account with a nonzero blocked_action_count is an operator decision, not a silent failure.
Keep the conversation going
These pages are meant to help operators solve real problems. If you want the next guide, grab the low-friction option. If you need the implementation, not just the guide, book time.
Get the next guide when it ships
I publish architecture guides grounded in real implementations. No generic AI filler.
Use your work email so I can keep the list useful and relevant.
Need the implementation, not just the guide?
Book a 15-minute working session with Mike right on his calendar. Tooling, consulting, or a mix of both is fine.
Open Mike's calendarIf you want me to come in with context, leave your email and a short note before the call.
The clean integration is the one you can audit six months later.
FoundryOps is strongest when Salesforce stays authoritative, Zendesk gets the context it needs, and every link can be audited, retried, and repaired with a plan-hash-protected receipt behind it.