ERRNA expert insight
Fiat On-Ramp and Off-Ramp Integration for Exchange Operators: Reconciliation Controls Before Go-Live
Fiat on-ramp and off-ramp integration is where a crypto exchange touches the banking and card world. That world has different clocks, different failure modes, and different people who can say "that payment never happened." For a Product Head or Exchange Operator, the question is not whether the provider's demo works. The question is: what does your platform do when the provider's answer is late, duplicated, contradictory, or missing?
This article lays out a proposed design for integrating a fiat ramp provider so that every payment, payout, and balance change ends in a state you can explain to an auditor, a customer, and your finance team. It covers durable operation identity, payload hashing, atomic work claiming, webhook intake, unknown-state handling, privacy ownership, and a set of mandatory go/no-go controls. It does not cover legal advice, provider selection by price, or any prediction about markets.
Two scope notes up front. First, the design is an engineering proposal, not a claim that any particular provider or Errna deployment already behaves this way. Second, the legal duties that apply to you (licensing, Travel Rule, card scheme rules, consumer protection, data protection) depend on your jurisdiction, business model, and whether you or the provider is the regulated party. Treat this as architecture guidance and confirm obligations with qualified counsel.
If you have already read our guides on idempotent withdrawals and webhooks, unknown withdrawal outcomes, and crypto deposit crediting, this piece covers the fiat side of the same problem: bank, card, and PSP rails, where "confirmed" can be reversed after the fact.
Key Takeaways
- A provider webhook is a hint, not the truth. Record it durably, then confirm authoritative state before moving money or balances.
- Never blindly retry an ambiguous timeout on a payout or payment. Reconcile the operation's authoritative state first. An unknown result means completion could not be confirmed, not success and not verified failure.
- Bind a durable, tenant-scoped operation ID to an immutable hash of the approved payload. Reject reuse of that ID with a changed payload. Treat a legitimate later payment as a new operation with its own ID.
- Claim work atomically in your own datastore. A read, compare, then write sequence is not compare-and-swap. Use a conditional update that succeeds for exactly one worker.
- Acknowledge a webhook only after dedupe, inbox record, and job scheduling are committed together. Otherwise the acknowledgment is a promise you cannot keep.
- Separate "credited" from "withdrawable." Fiat rails can return funds after apparent success, so release policy must be explicit.
- Decide privacy ownership per data flow. Identifiers, references, and even idempotency keys can be sensitive data.
- Go-live is all-required, not scored. One failed mandatory control means no-go, however good the other results look.
Why Fiat Ramp Integration Fails Differently From Crypto-Only Rails
Key takeaways: Crypto rails give you a public ledger you can query. Fiat rails give you a chain of intermediaries, each with its own status vocabulary and its own timeline. The danger is not that a payment fails. It is that you cannot tell whether it failed.
On a blockchain, the transaction hash is a shared reference and the state is independently observable, even if finality has its own subtleties. A fiat ramp is different. A user pays through a card network, a bank transfer, or an instant payment scheme. A provider or processor reports status. A bank or acquirer settles later. Any of these parties can report something that is later superseded by another party.
That produces several properties that matter for architecture:
- Status is asserted, not proven. You usually learn about a payment from the provider's API or webhook, not from a ledger you can verify yourself.
- Apparent success can be reversed. Depending on the rail, payments can be returned, disputed, or recalled after they look complete. The windows and rules depend on the rail, the provider, and your agreement. Get them in writing from your provider and bank rather than assuming.
- Two systems move value in opposite directions. Fiat arrives on one side and crypto leaves on the other. If either leg is duplicated or lost, you hold inventory risk or owe a customer.
- Personal and financial data is in the loop. Names, bank details, and references flow between you and the provider, which raises ownership questions that crypto-only flows rarely raise.
Where the Uncertainty Comes From
| Moment | What you know | What you do not know | Safe posture |
|---|---|---|---|
| You send a create-payment or payout request and the connection times out | Your request left your system | Whether the provider executed it | Mark the operation unknown. Look up by your own reference. Do not send a fresh request. |
| A webhook says "succeeded" | Some party sent a message | Whether it is authentic, current, in order, or a duplicate | Verify the signature per the provider's scheme, record it, then fetch authoritative state. |
| Funds show as settled | The provider reports settlement | Whether a return, dispute, or recall can still arrive | Credit according to a written release policy, not on the first positive signal. |
| A webhook never arrives | Nothing | Whether anything happened | Poll or reconcile on a schedule. Silence is not failure. |
This is why "integration" is the wrong frame. You are not connecting two APIs. You are building a small distributed system in which one participant is outside your control, and you must define what you believe at every moment and why.
How Most Teams Integrate, and Why the Happy Path Misleads
Key takeaways: The typical build is a payment button, a webhook handler, and a balance update. It works in the sandbox because sandboxes are polite. Production is not polite.
Most first integrations follow the same shape: the user starts a purchase, your backend calls the provider, the provider later calls your webhook with a success event, and your handler credits the balance or releases the crypto. Every step has a hidden assumption.
| Typical assumption | What production does instead | Resulting defect |
|---|---|---|
| Each webhook arrives once | Providers commonly retry, so the same event can arrive several times | Double credit, double release |
| Events arrive in order | A "completed" event can arrive before "pending" is processed | State regression, stuck operations |
| A timeout means the request failed | The provider may have executed it | Duplicate charge or duplicate payout after retry |
| Success is final | Returns and disputes can follow | Crypto already withdrawn against reversed funds |
| The user ID is a fine reference | References appear in statements, logs, and provider dashboards | Unintended disclosure of identifiers |
| One handler instance runs | Autoscaling and retries run several workers at once | Race conditions on the same operation |
The Retry Reflex
The most expensive habit is the retry reflex: a call times out, so the code calls again with a new request. For read operations that is harmless. For consequential mutations such as creating a payout, it can pay twice. Provider documentation is explicit on this point. Stripe's advanced error handling guidance, for example, tells integrators to treat the outcome of a request that returned a 500 as indeterminate, and not to retry with a new idempotency key until the original outcome has been resolved. That is one vendor's statement about its own API, but it expresses the right discipline for any provider: resolve the original outcome before you start another operation.
Also note what idempotency keys do not give you. Stripe's idempotent requests reference states that for its API v1 keys can be removed after at least 24 hours, that a key reused after pruning can create a new request, and that API v2 uses a 30-day replay window. Your own durable record must therefore outlive any provider's key retention. A provider's idempotency is a safety net for retries, not your system of record.
A Proposed Design: The Ramp Operation Ledger
Key takeaways: Give every fiat-affecting action one durable, tenant-scoped identity, one immutable approved payload, and one explicit state machine. Everything else (webhooks, polling, reconciliation) feeds that record. This is a proposed design for you to adapt and test, not a claim about an existing proprietary method.
Design Principles
- One operation, one identity. Generate an
operation_idonce, at the moment of user or operator intent. Scope it bytenant_idso that, in a multi-tenant or white-label setup, one tenant's key can never collide with or reveal another's. - Hash the approved payload. Serialize the approved request canonically (amounts as integer minor units, ISO currency code, destination token, quote ID, direction) and store a SHA-256 hash that cannot be updated. If the same
operation_idarrives with a different hash, reject it. Do not "fix" it. - Claim work atomically. Workers take ownership through a conditional update in your datastore, so only one succeeds.
- Use a closed state machine. Every transition names its allowed predecessor states. Anything else goes to review.
- Treat inbound events as evidence. Store them, deduplicate them, then confirm authoritative state before acting.
- Make the ledger entry unique per operation. A unique constraint on tenant, operation, and entry type is the last line of defense against double crediting.
Operation State Model
| State | Meaning | Allowed next states | Terminal? |
|---|---|---|---|
| READY | Approved payload stored, not yet sent | SUBMITTING, CANCELLED | No |
| SUBMITTING | A worker holds a lease and is calling the provider | PENDING_PROVIDER, FAILED_TERMINAL, UNKNOWN | No |
| PENDING_PROVIDER | Provider acknowledged with a reference | CONFIRMED, FAILED_TERMINAL, UNKNOWN | No |
| UNKNOWN | Completion could not be confirmed | PENDING_PROVIDER, CONFIRMED, FAILED_TERMINAL, MANUAL_REVIEW | No |
| CONFIRMED | Provider reports success; release policy applies | SETTLED, RETURNED | No |
| SETTLED | Settlement matched to bank or provider statement | RETURNED (only within the written return window) | Effectively, after the window |
| RETURNED | Funds reversed, disputed, or recalled | RECOVERY_IN_PROGRESS | No |
| FAILED_TERMINAL | Verified failure, with evidence recorded | None | Yes |
| MANUAL_REVIEW | Named owner must decide | Any, by audited action | No |
Notice that UNKNOWN is a first-class state. It is not a synonym for FAILED and it must never trigger an automatic resend. Its only exits are evidence: a provider lookup by your reference, a matching event, a statement line, or a human decision recorded with a reason.
Illustrative Schema and Claim Logic
The following code is illustrative PostgreSQL-style SQL. It is not an official API contract for any provider, and it must be adapted, load-tested, and reviewed before use.
-- Illustrative only. Adapt to your datastore and review before use. CREATE TABLE ramp_operation ( tenant_id uuid NOT NULL, operation_id uuid NOT NULL, -- durable identity created at intent time direction text NOT NULL, -- 'ON_RAMP' or 'OFF_RAMP' payload_hash bytea NOT NULL, -- SHA-256 of canonical approved payload; never updated state text NOT NULL, version integer NOT NULL DEFAULT 0, claimed_by text, lease_until timestamptz, provider_ref text, PRIMARY KEY (tenant_id, operation_id) ); -- Intake: create or find. Same key + different hash must be rejected by the caller. INSERT INTO ramp_operation (tenant_id, operation_id, direction, payload_hash, state) VALUES ($1, $2, $3, $4, 'READY') ON CONFLICT (tenant_id, operation_id) DO NOTHING; -- Then SELECT payload_hash for (tenant_id, operation_id). The stored hash is immutable, -- so a mismatch means a changed payload: return a conflict, do not submit. -- Atomic claim: succeeds for exactly one worker (check affected rows = 1). UPDATE ramp_operation SET state = 'SUBMITTING', claimed_by = $5, lease_until = now() + interval '2 minutes', version = version + 1 WHERE tenant_id = $1 AND operation_id = $2 AND state = 'READY'; -- Guarded transition: target-side conditional mutation, not read-compare-then-write. UPDATE ramp_operation SET state = $6, version = version + 1 WHERE tenant_id = $1 AND operation_id = $2 AND state = ANY($7) -- allowed predecessor states AND version = $8; -- zero rows: already applied, or conflict. Re-read, route to review. -- Last line of defense against double credit. CREATE UNIQUE INDEX ux_ledger_once ON ledger_entry (tenant_id, operation_id, entry_type);Three details deserve emphasis. First, if a worker dies while an operation is SUBMITTING and the lease expires, move it to UNKNOWN, not back to READY. The request may have reached the provider. Second, the conditional UPDATE ... WHERE state = ... is the compare-and-swap, because the check and the write happen in one atomic statement in the datastore that owns the truth. Third, the provider call itself is not a compare-and-swap unless the provider offers preconditions. You rely on provider-side idempotency plus lookup by your own reference, and you disclose that limit. If a provider offers neither idempotent creation nor lookup by merchant reference, treat consequential payouts through that provider as a no-go.
Choosing the Provider Idempotency Key
Derive the key sent to the provider from the opaque operation_id, never from an email, name, or account number. Stripe's documentation specifically advises avoiding sensitive data such as email addresses or personal identifiers as idempotency keys. The same logic applies to merchant references that will appear on bank statements and provider dashboards.
A note on standards scope: the IETF Idempotency-Key HTTP header draft describes a header for making POST and PATCH requests fault-tolerant, and states that a key must not be reused with a different request payload. At the time of writing it is an active Internet-Draft, not a published RFC, and it applies to HTTP request headers. It does not govern inbound webhooks, queue messages, or bank files, which each need their own deduplication identity.
Handling Unknown, Terminal, and Reversible Outcomes
Key takeaways: Every ambiguous input needs a pre-decided path: verify, quarantine, reconcile, or escalate. "Retry and hope" is not a path. And every operation must be able to reach a terminal state with a named owner.
Webhook Intake: Acknowledge Only What You Can Keep
A 2xx response tells the provider "I have this, stop sending it." If your handler returns 2xx and then crashes before the work is durable, the provider will not resend and the event is gone. The fix is atomicity. Perform these steps in one database transaction, or through a transactional outbox, and only then respond:
- Verify the signature or authentication scheme the provider documents. If verification fails, reject. Do not acknowledge.
- Insert into an inbox table keyed by tenant, provider, and provider event ID, ignoring duplicates.
- If the row was newly inserted, schedule a "fetch authoritative state and apply" job in the same transaction.
- Commit, then return 2xx.
A duplicate event finds the inbox row already present, schedules nothing new, and still receives 2xx, because the work is already recorded. That is verified acknowledgment: the 2xx means "durably recorded and scheduled," not "received."
Uncertain Input Handling
| Input condition | Wrong response | Required response |
|---|---|---|
| Signature invalid or missing | Process anyway, log a warning | Reject, alert, never touch state |
| Event references an operation you cannot find | Drop it | Store in a quarantine table, alert, hold acknowledgment policy per provider contract, investigate |
| Event type or status is not in your mapping | Default to success or failure | Quarantine, map explicitly after review, never guess |
| Event is older than current state | Overwrite state | Record it, ignore for transition, keep audit trail |
| Event amount or currency differs from the approved payload | Trust the event | Move to MANUAL_REVIEW, do not credit |
| Provider API lookup times out | Assume failure | Remain UNKNOWN, schedule bounded re-check |
Reconciling an Ambiguous Timeout
When a create-payout call times out, follow this order and do not skip steps:
- Mark the operation UNKNOWN. Do not release any dependent crypto or balance.
- Look up the payout by your merchant reference or the provider's idempotency-keyed retrieval, if the provider supports it.
- If found, adopt the provider's state through a guarded transition.
- If a documented "not found" is returned, only then consider a replay, using the same idempotency key and the same payload, and only inside the provider's documented retention window.
- If the window has passed or the provider's answer is unclear, stop automatic action and escalate to MANUAL_REVIEW with the statement-level evidence.
Our companion article on unknown withdrawal outcomes applies the same discipline to on-chain sends.
Terminal Failure Needs Evidence and an Owner
FAILED_TERMINAL should require recorded evidence: a provider status, a documented decline code, or a reviewer decision. Operations that retry forever are a hidden liability, so every non-terminal state needs a maximum age after which it escalates to a named queue. Dead-letter queues without an owner are just slower data loss.
Credited Versus Withdrawable
Because fiat rails can reverse, split your balance semantics. A credited balance lets a user see and trade funds. A withdrawable balance is released under a written policy that reflects the rail's reversal characteristics, the user's risk profile, and your agreement with the provider. Define that policy with your finance, risk, and compliance owners, and record which rule released each amount.
Privacy Ownership
Every ramp flow shares data. Before launch, produce a data-flow map that answers: which party decides why and how each data element is processed, which party processes on whose instructions, where the data is stored, how long it is retained, and how deletion requests interact with financial record-keeping duties. In frameworks such as the EU General Data Protection Regulation, roles such as controller and processor carry specific obligations, and whether they apply to you depends on your facts. Also remember that identifiers can themselves be sensitive: operation IDs that embed user IDs, references that contain account fragments, and logs that capture webhook bodies all widen your exposure. Our KYC and AML compliance page describes the compliance workstream these questions feed into, and the data protection page describes how Errna handles data protection.
Why This Fails in the Real World
Key takeaways: Capable teams fail here for system reasons: ownership gaps between engineering, finance, and compliance; sandboxes that never produce ambiguity; and metrics that count successes but not unknowns. The scenarios below are illustrative composites, not customer results.
Scenario 1: The Timeout That Paid Twice (Illustrative)
An exchange pays out fiat for off-ramp sales. During a provider slowdown, the payout call times out. The worker's generic retry wrapper creates a fresh request with a new reference, because "a new request is simpler than tracking the old one." The original request had actually succeeded. Two payouts land with the customer. Support finds out when the customer, honestly, mentions it. Recovering the second payout depends on the customer's cooperation and the rail's recall rules.
Why intelligent teams still fail: the retry wrapper was written for read calls and later reused for payouts. Nobody owned the question, "which operations are consequential?" The sandbox never timed out after executing. No metric counted unknown outcomes, so the problem had no number attached to it.
System fix: a durable operation ID, an immutable payload hash, a ban on creating new references for the same intent, and an UNKNOWN state that blocks resend until lookup resolves it.
Scenario 2: The Credit Before the Return (Illustrative)
A platform credits a user and permits an immediate crypto withdrawal as soon as the provider's first success event arrives. Days later, the funding payment is reversed. The crypto is long gone, and the platform now carries a loss and a difficult customer conversation.
Why intelligent teams still fail: product wanted a fast first-deposit experience, risk was consulted late, and "succeeded" was treated as a single bit rather than a stage in a lifecycle. Finance saw the loss in a monthly close, long after engineering considered the integration done.
System fix: separate credited and withdrawable balances, a written release policy, reversal events mapped to RETURNED with a recovery workflow, and a shared dashboard where finance, risk, and engineering see the same operation states.
Scenario 3: The Tenant Collision (Illustrative)
In a white-label deployment serving several operators, idempotency keys are stored globally. Two tenants generate the same short, human-readable reference format. One tenant's operation is mistaken for the other's duplicate and is silently skipped, or worse, returns the other tenant's stored response.
Why intelligent teams still fail: single-tenant testing never exercised the collision, and "unique" was assumed rather than enforced by schema.
System fix: tenant-scoped primary keys, tenant-scoped inbox keys, and tests that deliberately reuse identifiers across tenants.
Common Failure Signals
- Support tickets that begin with "I was charged but my balance did not change."
- A reconciliation spreadsheet that exists "just in case."
- No metric for operations stuck in a non-terminal state beyond a defined age.
- Webhook handlers that return 200 before writing anything durable.
- No one can say who owns a quarantined event.
Mandatory Go/No-Go Controls and Delivery-Model Trade-Offs
Key takeaways: Production readiness is not a score. It is a set of controls that must all be true. If one fails, you do not launch, regardless of how many others pass. Your delivery model (custom build, white-label, or SaaS) changes who owns each control, not whether it is required.
Go/No-Go Checklist (All Required)
This checklist is a proposed review aid. Passing it does not certify your platform as compliant or production-ready, and it does not replace independent security review, legal advice, or provider due diligence.
| # | Control | Evidence to show |
|---|---|---|
| 1 | Durable, tenant-scoped operation identity exists before any provider call | Schema with tenant-scoped primary key, tests that reuse an ID across tenants |
| 2 | Immutable approved-payload hash; changed-payload reuse is rejected | Test that sends the same ID with a modified amount or destination and receives a conflict |
| 3 | Atomic work claim via conditional mutation in the owning datastore | Concurrency test with many workers on one operation, exactly one claim succeeds |
| 4 | Webhook acknowledgment occurs only after dedupe, inbox record, and job scheduling are atomic or explicitly recoverable | Crash-injection test between receipt and commit |
| 5 | Signature or authentication verification; invalid input never changes state | Negative tests with tampered bodies |
| 6 | UNKNOWN is a state; ambiguous timeouts block resend until lookup or review resolves them | Fault test that times out after provider execution |
| 7 | Provider supports idempotent creation and lookup by your reference, or consequential payouts are out of scope | Provider documentation and sandbox evidence, recorded |
| 8 | Every non-terminal state has a maximum age and a named owner queue | Alert configuration and runbook |
| 9 | Three-way reconciliation: internal ledger, provider records, bank or settlement statements, with break handling | Sample reconciliation run with a deliberately injected break |
| 10 | Credited and withdrawable balances are separate, with a written release policy | Policy signed by finance, risk, and compliance owners |
| 11 | Unique ledger constraint prevents double credit or double debit per operation | Replay test of the same confirmation event |
| 12 | Privacy ownership documented per data flow; no sensitive data in keys, references, or logs | Data-flow map, log sampling, provider agreement review |
| 13 | Reversal and recovery workflow exists and has been exercised | Tabletop or sandbox exercise record |
Comparing Delivery Models
| Dimension | Custom build and integrate | White-label exchange software | SaaS exchange |
|---|---|---|---|
| Who writes the operation ledger and state machine | You | Shared: vendor core, your customizations | Vendor, with your configuration |
| Control over provider choice and contract | Highest | Depends on what is pre-integrated and what can be added | Usually the most constrained |
| Speed to first launch | Slowest | Faster | Fastest |
| Hidden failure modes | Under-tested edge cases, staffing gaps | Customizations that bypass vendor controls, tenant isolation assumptions | Opaque failure handling, limited access to evidence for audits |
| Scalability of operations | Yours to engineer | Largely vendor-defined, extensible | Vendor-defined |
| What you must still own | Everything on the checklist | Release policy, reconciliation process, privacy roles, evidence | Release policy, reconciliation review, privacy roles, vendor oversight |
No model removes the checklist. In a vendor-supplied model, convert each control into a question for the vendor and ask for evidence, not assurances. Errna's white-label crypto exchange and crypto exchange development offerings include fiat and crypto trading support in their product descriptions. Which providers, rails, and controls are in scope for a given deployment is defined in the engagement, and you should verify each control above against that scope.
Trade-Offs to Accept Openly
- Speed versus certainty. Holding withdrawals until a release condition is met frustrates users. Offer tiered release, not blanket speed.
- Operational load. UNKNOWN and MANUAL_REVIEW queues need people. Illustratively, if a hypothetical platform runs 1,000 ramp operations a day and 0.3% land in review, that is about three cases a day. The point is not the number, which is invented for illustration, but that you must measure your own rate and staff for it.
- Provider dependency. Your design is only as strong as the provider's lookup and idempotency features. Test these before signing.
A Lower-Risk Rollout Path and Annual Re-Verification
Key takeaways: Roll out in stages that expose ambiguity early, with small exposure. Re-verify provider behavior and standards status regularly, because the assumptions that made your design safe can change without a code change on your side.
Staged Rollout
- Contract and documentation review. Collect written answers on idempotency retention, lookup by reference, webhook retry behavior and ordering, return and dispute windows, settlement timing, and data roles. Record them in a decision file.
- Sandbox fault testing. Simulate timeouts after execution, duplicate and out-of-order events, tampered signatures, and unknown event types. Sandboxes rarely produce these on their own, so inject them.
- Shadow mode. Run the new flow without releasing funds, comparing your ledger to provider records for a defined period.
- Limited live launch. Cap per-operation and per-day amounts, restrict to lower-risk cohorts, and require manual approval on payouts above a threshold set by your risk owner.
- Widen under evidence. Expand caps only when reconciliation breaks, unknown-state ages, and manual-review volumes meet thresholds your owners have set in advance.
Metrics Worth Tracking
| Metric | Why it matters |
|---|---|
| Operations in UNKNOWN beyond maximum age | Direct measure of unresolved exposure |
| Reconciliation breaks per period, by cause | Shows whether defects are in code, provider data, or process |
| Quarantined events not yet mapped | Shows mapping gaps and provider changes |
| Returned or reversed operations after credit | Tests the release policy against reality |
| Duplicate-event rate | Confirms dedupe is being exercised |
2027 Update: What to Re-Verify
As of this writing, the IETF Idempotency-Key header work is still an active Internet-Draft, so check its current status before citing it as a standard. Provider idempotency windows have also differed between API versions (for example, Stripe documents 24 hours for API v1 and 30 days for v2), so re-read your provider's current documentation whenever you change API versions. Regulators continue to refine expectations for virtual asset service providers, and the FATF virtual assets page is a starting point for tracking international AML standards, though your legal duties come from your own jurisdictions. The evergreen principle does not change: durable identity, immutable payload, atomic claim, verified acknowledgment, explicit unknown state, and evidence-based terminal states.
Not sure your ramp integration can explain every unknown payment?
Bring your provider contract, state model, and reconciliation process. We will review them against the go/no-go controls above.
Talk to Errna's blockchain and exchange engineering team about fiat ramp integration.
Contact UsFor scope details on integrations, see Errna's on and off ramp integration service page.
Conclusion: Five Decisions to Make Before Go-Live
A fiat ramp integration is ready when you can explain, for any payment, what you believe, why you believe it, and who owns the next step. These actions turn that standard into decisions:
- Name the consequential operations. List every action that moves fiat or releases crypto, and forbid generic retry wrappers on them.
- Implement the operation ledger. Tenant-scoped identity, immutable payload hash, atomic claim, guarded transitions, and a unique ledger constraint.
- Obtain written provider answers. Idempotency retention, lookup by reference, webhook behavior, return windows, and data roles. If lookup or idempotent creation is missing, exclude consequential payouts from scope.
- Write the release policy and the ownership map. Credited versus withdrawable rules, owners for UNKNOWN and quarantine queues, and a privacy data-flow map.
- Run the checklist as all-required. Any failed control means no-go until it is fixed and evidenced.
About Errna
Errna is a blockchain, cryptocurrency, and digital asset technology company that has operated since 2003, with more than 1,000 in-house professionals and a delivery history that includes exchange software, enterprise blockchain, and compliance-oriented platform work. Errna's published credentials include ISO 27001, CMMI Level 5, and SOC 2; these describe the company's own delivery and security processes and do not certify any client's platform, the guidance in this article, or a reader's legal compliance. This article is not legal, financial, or investment advice. Reviewed by Errna Expert Team.
Frequently Asked Questions
What is the difference between a fiat on-ramp and an off-ramp for a crypto exchange?
An on-ramp converts fiat money into crypto or exchange balance, usually by card, bank transfer, or another payment method. An off-ramp does the reverse: it converts crypto or balance into fiat paid out to a bank account or similar destination. They carry different risks. On-ramps are exposed to reversals after apparent success. Off-ramps are exposed to duplicate or misdirected payouts, which are hard to recall.
Can I safely retry a payout request that timed out?
Not blindly. A timeout means you do not know whether the provider executed the request. Mark the operation unknown, look up its state using your own reference or the provider's retrieval mechanism, and only replay with the same idempotency key and identical payload if the provider confirms it has no record and the key is still within its documented retention window. If you cannot establish the outcome, escalate to manual review.
Does the Idempotency-Key HTTP header standard solve duplicate payments?
- It is an IETF Internet-Draft for HTTP requests, not a published RFC at the time of writing, so confirm its status before citing it as a standard.
- It helps only if the provider's server implements and documents it.
- It does not cover inbound webhooks, queues, or bank files, which need their own deduplication keys.
- It does not replace your own durable operation record, because provider key retention is finite.
How should a white-label exchange keep tenants from colliding on idempotency keys?
Scope every identity by tenant: make the primary key a combination of tenant and operation ID, scope webhook inbox keys by tenant and provider, and test deliberate identifier reuse across tenants. Also confirm that stored responses are never served across tenants. Ask the vendor for evidence of these controls rather than a general assurance.
Is a webhook enough to credit a user's balance?
It should not be the only input. Verify the signature, record the event durably, deduplicate it, and fetch authoritative state from the provider before applying a guarded state transition. Then credit through a ledger entry that is unique per operation. Whether funds are also withdrawable should follow a written release policy that reflects reversal risk on the rail used.
Who owns privacy responsibilities when a ramp provider handles customer data?
It depends on the data flow, your contracts, and the laws that apply to you. Roles such as controller and processor, where those concepts apply, are determined by who decides the purposes and means of processing, not by labels in a sales deck. Document each flow, review provider agreements with qualified counsel, and avoid placing sensitive identifiers in keys, references, or logs.
Can a go/no-go checklist certify that my ramp integration is production-ready?
No. A checklist of mandatory controls helps you find blocking gaps, but passing it does not certify security, compliance, or readiness. Use it alongside independent security testing, legal review, provider due diligence, and your own risk acceptance process.
Fiat is where exchange launches quietly go wrong.
Duplicate payouts, credits before returns, and tenant collisions rarely show up in a demo. They show up in month three.