OnRampDLT Developer Docs
Complete reference for the OnRampDLT platform: XRPL-native token issuance, private Reg D bond offerings, authentication, issuer wallet management, KYC compliance scope, and Stripe billing. All XRPL transactions are constructed server-side and signed client-side via Xaman — private keys never leave your device.
https://api.onrampdlt.com
Quickstart
Three steps to issue your first token:
- Create an account —
POST /api/auth/signupand store the returned JWT. - Register your issuer wallet —
POST /api/issuer/walletwith your Xaman XRPL r-address. Sign theAccountSettransaction returned to enable DefaultRipple. - Mint a token —
POST /api/mintwithtokenName,tokenSymbol, andnetwork: "xrpl".
For bond issuance you will need a Pro plan subscription and a funded issuer wallet (minimum 12 XRP). See the Bond Platform section.
Authentication
All protected endpoints require a valid JWT sent as a Bearer token. The token is returned in the response body at signup and login.
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
localStorage and always send it via the Authorization: Bearer header. Cookie auth (auth_token HttpOnly) is supported as a fallback for non-Safari browsers only.JWTs expire after 30 days and are validated server-side on every request. A token that has been logged out is immediately invalid even if not yet expired. Re-authenticate at POST /api/auth/login to get a fresh token.
Rate Limits
Rate limits are enforced per IP via Cloudflare KV. Exceeding a limit returns HTTP 429 Too Many Requests.
| Endpoint | Limit | Window | Notes |
|---|---|---|---|
| POST /api/auth/login | 5 requests | 15 minutes | Per IP. Protects against credential stuffing. |
| POST /api/auth/signup | 5 requests | 15 minutes | Per IP. Turnstile adds bot protection layer. |
| POST /api/auth/resend-verification | 3 requests | 1 hour | Per email address. Prevents verification email abuse. |
| All other endpoints | No hard app limit | — | Cloudflare infrastructure-level limits apply for abnormal traffic. |
{ "error": "Too many login attempts. Try again later." }POST /api/auth/signup
Create a new user account. Returns a JWT and user record. Supports optional Cloudflare Turnstile verification.
| Field | Type | Description |
|---|---|---|
| emailreq | string | User email address |
| passwordreq | string | Plaintext password (hashed server-side with PBKDF2/SHA-256) |
| marketingConsentopt | boolean | Marketing email opt-in |
| turnstileTokenopt | string | Cloudflare Turnstile challenge token |
{ "success": true, "token": "eyJ...", "user": { "id": 42, "email": "user@example.com" } }
auth_token HttpOnly cookie for browser clients. Use the body token for localStorage-based auth on Safari and mobile.POST /api/auth/login
Authenticate an existing user. Rate-limited to 5 attempts per 15 minutes per IP.
| Field | Type | Description |
|---|---|---|
| emailreq | string | Registered email |
| passwordreq | string | Account password |
| turnstileTokenopt | string | Cloudflare Turnstile token |
{ "success": true, "token": "***", "user": { "id": 42, "email": "user@example.com" } }
HTTP 403 with { "error": "...", "email_unverified": true, "email": "user@example.com" }. Clients should detect email_unverified: true and surface a resend verification UI. Use POST /api/auth/resend-verification to send a new verification link.POST /api/auth/logout
Revoke the current JWT and clear the auth cookie. The token is immediately invalid on all subsequent requests — both Bearer header and cookie — regardless of remaining expiry. Revocation is enforced server-side via a KV blacklist with TTL matching the token's remaining validity.
No request body required. Send the JWT in the Authorization header as usual.
{ "success": true }
POST /api/auth/resend-verification
Resend the email verification link to a registered email address. Intended for use when POST /api/auth/login returns HTTP 403 with email_unverified: true. No authentication is required — this endpoint is pre-login. To prevent user enumeration, the response is always { "success": true } regardless of whether the email exists on the platform.
| Field | Type | Description |
|---|---|---|
| emailreq | string | The email address to send the verification link to |
{ "success": true, "message": "If that email is registered and unverified, a verification link has been sent." }
HTTP 429. The verification link expires after 24 hours.{ "success": true } — the API does not reveal whether the email address is registered on the platform.GET /api/auth/me
Return the authenticated user's profile, tier, KYC status, and token mint count. Requires a valid JWT.
{
"id": 42,
"email": "user@example.com",
"tier": "investor",
"first_name": "Jane",
"last_name": "Smith",
"display_name": "JaneSmith",
"mintCount": 0,
"kyc_status": "verified",
"accredited_investor": true,
"created_at": "2026-04-01T00:00:00Z"
}
first_name, last_name, and display_name are null until set via PUT /api/auth/me. These are user-editable display fields, distinct from the legally binding identity in the KYC self-certification record.true if the user has at least one bond subscription on record, regardless of current tier. Dashboards should use this to surface the holdings view for lapsed Investor accounts rather than hiding it. Downgrading from Investor to Free never removes investment records.| Value | Wallets | Can Invest | Tokens | Bonds | API |
|---|---|---|---|---|---|
| free | 0 | No | No | No | No |
| investor | 1 | Yes | No | No | No |
| starter | 1 | Yes | Unlimited | No | No |
| pro | 3 | Yes | Unlimited | 506(b) | No |
| business | 10 | Yes | Unlimited | 506(b) + 506(c) | Yes |
| enterprise | Unlimited | Yes | Unlimited | 506(b) + 506(c) | Yes |
| Value | Meaning |
|---|---|
| not_started | No KYC or accreditation on file |
| verified | Self-certification complete (506(b)) or Stripe Identity verified (506(c)) |
| expired | KYC verification has passed its validity window |
| suspended | Account suspended — contact support |
PUT /api/auth/me
Update the authenticated user's display profile. Returns the updated user object in the same shape as GET /api/auth/me.
| Field | Type | Max Length | Description |
|---|---|---|---|
| first_name | string | 80 | Given name |
| last_name | string | 80 | Family name |
| display_name | string | 80 | Public display name used in the dashboard |
"" to clear a field back to null. At least one field must be present or the request returns 400.{ "id": 42, "email": "user@example.com", "tier": "investor", "first_name": "Jane", "last_name": "Smith", "display_name": "JaneSmith", ... }
GET /api/tier/features
Return the feature flag set for the authenticated user's current tier. Useful for gating UI elements without re-checking tier strings.
{
"tier": "investor",
"can_invest": true,
"token_issuance": false,
"bonds_506b": false,
"bonds_506c": false,
"api_access": false,
"wallet_limit": 1,
"team_seats": 0
}
{ "error": "...", "upgrade_required": true, "required_tier": "X" } with HTTP 403 when a lower-tier user attempts access.XAMAN Wallet Authentication
XAMAN (formerly XUMM) is used for QR-based XRPL wallet authentication. Create a payload, display the QR or deep link, then poll status until the user signs.
POST /api/auth/xaman/payload
| Field | Type | Description |
|---|---|---|
| actionreq | string | Must be signin |
{
"uuid": "599f5ffc-a258-4662-8b91-2aebffa63045",
"qr_png": "https://xumm.app/sign/599f5ffc-a258-4662-8b91-2aebffa63045_q.png",
"deep_link": "https://xumm.app/sign/599f5ffc-a258-4662-8b91-2aebffa63045"
}
Display qr_png as an image for desktop scanning. Use deep_link on mobile to open XAMAN directly. Poll GET /api/auth/xaman/status/:uuid until resolved.
GET /api/auth/xaman/status/:uuid
{ "status": "pending" }
{ "status": "resolved", "account": "rJtG6NREp9FSnKRra7ADmbM5vtw5jAqFDF" }
resolved, account contains the verified XRPL wallet address. On expired, create a new payload.KYC & Accreditation API
The KYC API records accreditation self-certifications for 506(b) bond subscriptions. Full Stripe Identity verification ($6 add-on) is required for 506(c) bond access. Auth required for all endpoints.
GET /api/kyc/status
{
"kyc_status": "not_started",
"accredited_investor": false,
"accredited_method": null,
"kyc_verified_at": null,
"kyc_expires_at": null,
"days_until_expiry": null
}
POST /api/kyc/self-certify
Submit accredited investor self-certification. Creates a verifiable on-platform record with timestamp and IP. Required before subscribing to 506(b) bond offerings.
| Field | Type | Description |
|---|---|---|
| full_namereq | string | Full legal name |
| date_of_birthreq | string | Format: YYYY-MM-DD |
| address_line1req | string | Street address |
| address_line2opt | string | Suite, apt, etc. |
| cityreq | string | City |
| state_provinceopt | string | State or province |
| postal_codereq | string | ZIP or postal code |
| countryopt | string | ISO country code (default: US) |
| accredited_basisreq | string | income | net_worth | professional |
{
"success": true,
"kyc_status": "verified",
"accredited_method": "self_cert",
"message": "Self-certification complete. You can now subscribe to 506(b) bond offerings you have been invited to."
}
Wallet Management
Register and manage XRPL wallets associated with a user account. Wallet limits are enforced by tier. Auth required for all endpoints.
GET /api/wallets
{ "wallets": [
{
"id": 1,
"xrpl_address": "rJtG6NREp9FSnKRra7ADmbM5vtw5jAqFDF",
"label": "My Main Wallet",
"wallet_type": "xaman",
"network": "mainnet",
"is_default": 1,
"verified": 1,
"created_at": "2026-04-01T00:00:00Z"
}
] }
wallets array — not a plain array.POST /api/wallets
Register a new XRPL wallet. Requires Starter tier or above. Wallet limits: free=0, starter=1, pro=3, business=10.
| Field | Type | Description |
|---|---|---|
| xrpl_addressreq | string | Classic r-address (25–34 chars, starts with r) |
| labelopt | string | Display label, max 50 chars (default: "My Wallet") |
| wallet_typeopt | string | xaman | crossmark | gemwallet (default: xaman) |
| networkopt | string | mainnet | testnet (default: testnet) |
| set_as_defaultopt | boolean | Set as the default wallet for this account |
{ "success": true, "wallet": { "id": 1, "xrpl_address": "rXXX...", "label": "My Wallet", "wallet_type": "xaman", "network": "testnet", "is_default": 1 } }
{ "error": "...", "upgrade_required": true, "required_tier": "starter" }. At limit: returns 403 with current_count and limit fields.Issuer Profile
Issuer profiles are public-facing company records that appear on bond listings and investor due diligence pages. Auth required to create or update.
GET /api/issuer/profile
{ "profile": { "company_name": "Acme Capital LLC", "tagline": "Real Assets, Real Returns", "description": "...", "website_url": "https://acme.com", "logo_url": null, "linkedin_url": null, "twitter_url": null, "verified": false } }
Returns { "profile": null } if no profile has been created yet.
POST /api/issuer/profile & PUT /api/issuer/profile
Create or update the issuer profile. Both methods perform an upsert. Auth required.
| Field | Type | Description |
|---|---|---|
| company_name | string | Legal entity or trade name |
| tagline | string | Short tagline shown on listings |
| description | string | Full company description for investors |
| website_url | string | Company website URL |
| logo_url | string | Logo image URL |
| linkedin_url | string | LinkedIn company page URL |
| twitter_url | string | Twitter/X profile URL |
{ "success": true, "profile": { "company_name": "Acme Capital LLC", ... } }
POST /api/issuer/wallet
Register a Xaman XRPL wallet as the authenticated user's bond issuer wallet. Each account can have one issuer wallet. The wallet must be funded with at least 12 XRP before bonds can be activated.
| Field | Type | Description |
|---|---|---|
| xrplAddressreq | string | Classic r-address (starts with r, 25-34 chars) |
| displayNameopt | string | Public display name for the issuer |
{
"id": "a1b2c3d4e5f6",
"xrplAddress": "rXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"funded": false,
"message": "Fund with at least 12 XRP before activating bonds.",
"nextStep": "Enable DefaultRipple via Xaman, then POST /api/bonds",
"xamanAccountSet": {
"txjson": { "TransactionType": "AccountSet", "Account": "rXXX...", "SetFlag": 8 },
"instructions": "Sign in Xaman to enable token issuance"
}
}
AccountSet transaction returned here using Xaman. This enables rippling on your wallet.GET /api/issuer/wallet
Retrieve the authenticated user's registered issuer wallet, including live XRP balance fetched from XRPL and bond count.
{
"id": "a1b2c3d4e5f6",
"xrplAddress": "rXXX...",
"funded": true,
"xrpBalance": 45.12,
"accountExists": true,
"minRequired": 12,
"bondCount": 2,
"xrplExplorer": "https://livenet.xrpl.org/accounts/rXXX..."
}
{ "wallet": null } with a 200 status.POST /api/mint
Mint a new XRPL token. Free-tier accounts are limited to 1 token. Pro accounts have unlimited issuance. The API records the mint and returns a transaction reference.
| Field | Type | Description |
|---|---|---|
| tokenNamereq | string | Full token name (e.g. "MyToken") |
| tokenSymbolreq | string | Ticker symbol (e.g. "MTK") |
| networkreq | string | Must be xrpl |
{
"success": true,
"token": {
"name": "MyToken",
"symbol": "MTK",
"network": "xrpl",
"txHash": "xrpl_1711234567890_a1b2c3"
}
}
GET /api/mints
List all token mints for the authenticated user, ordered by creation date descending.
[
{
"id": 1,
"token_name": "MyToken",
"token_symbol": "MTK",
"network": "xrpl",
"tx_hash": "xrpl_1711234567890_a1b2c3",
"created_at": "2026-01-15T10:30:00Z"
}
]
Bond Platform
The bond platform enables Reg D-compliant private bond offerings on the XRPL. Bonds are represented as XRPL trust lines — investors hold bond tokens issued by the offering wallet. All signing is done client-side via Xaman; the API only records metadata and verifies payments on-chain.
POST /api/bonds
Create a new bond offering in draft status. The bond is not visible to investors until activated.
| Field | Type | Description |
|---|---|---|
| namereq | string | Bond name (e.g. "Series A Bond") |
| tickerreq | string | Bond token symbol — max 8 chars. Tickers longer than 3 chars are auto-hex-encoded for XRPL. |
| totalUnitsreq | number | Total bond units available for subscription |
| priceXrpreq | number | Price per unit in XRP |
| interestRateBpsreq | number | Annual interest rate in basis points (e.g. 800 = 8%) |
| termDaysreq | number | Bond term length in days |
| paymentSchedulereq | string | monthly | quarterly | semiannual | annual |
| xrpRewardBpsopt | number | Early-investor XRP reward in basis points of invested amount |
| descriptionopt | string | Bond description shown to investors |
| issuerDisplayNameopt | string | Public issuer name |
{
"id": "a1b2c3d4e5f67890",
"status": "draft",
"name": "Series A Bond",
"ticker": "SRAB",
"tickerHex": "5352414200000000000000000000000000000000",
"totalUnits": 1000,
"priceXrp": 100,
"interestRateBps": 800,
"termDays": 365,
"paymentSchedule": "quarterly",
"metrics": {
"totalRaiseXrp": 100000,
"annualInterestXrp": "8000.00",
"totalXrpRewardPool": "500.00",
"recommendedWalletFunding": 8520
},
"nextStep": "Fund your issuer wallet with at least 520 XRP, then PUT /api/bonds/:id/activate"
}
GET /api/bonds
List bonds by status. Defaults to active. Returns up to 50 bonds.
| Param | Default | Options |
|---|---|---|
| status | active | draft | active | closed |
{ "bonds": [ { ... } ], "count": 3 }
GET /api/bonds/mine
List all bonds created by the authenticated issuer.
{ "bonds": [ { ... } ], "count": 2 }
GET /api/bonds/my-investments
Return all bond subscriptions belonging to the authenticated user, joined with bond details. No tier gate — available to any authenticated user regardless of current plan, including Free accounts. This endpoint is intentionally ungated so that lapsed Investor subscribers retain permanent access to their holding records.
{
"investments": [
{
"subscription_id": 1,
"bond_id": "a1b2c3d4",
"investor_address": "rXXX...",
"amount": "500.00",
"tx_hash": "ABCD1234...",
"subscription_status": "confirmed",
"subscribed_at": "2026-04-01T00:00:00Z",
"bond_name": "Series A Bond",
"symbol": "SRAB",
"bond_type": "506b",
"interest_rate": "8.00",
"duration_months": 24,
"bond_status": "active",
"issuer_address": "rISSUER...",
"network": "mainnet"
}
],
"count": 1
}
GET /api/bonds/:id
Public bond detail view. Includes holder count, amount raised, payment schedule, and subscription instructions when the bond is active.
{
"id": "a1b2c3d4e5f67890",
"status": "active",
"name": "Series A Bond",
"holderCount": 12,
"amountRaisedXrp": "12000.00",
"payments": [ { "type": "interest", "scheduledDate": "...", "xrpPerUnit": "2.00", "status": "pending" } ],
"subscribeInstructions": {
"step1": "Sign a TrustSet to the issuer for the bond currency",
"step2": "Send 100 XRP per unit to the issuer wallet",
"step3": "POST /api/bonds/:id/subscribe with address, units, and payment tx hash"
}
}
PUT /api/bonds/:id/activate
Activate a draft bond and open it for subscriptions. The issuer must sign an AccountSet transaction (SetFlag 8 — DefaultRipple) in their XRPL wallet and submit the transaction hash. DefaultRipple is required on the issuer wallet before bond tokens can be distributed to investors. Verifies the issuer wallet is funded with at least 12 XRP. Builds the full payment schedule on activation. Bond moves from draft to active status. Auth required — issuer must own the bond.
| Field | Type | Description |
|---|---|---|
| accountsetTxhashreq | string | XRPL transaction hash of the signed AccountSet transaction (SetFlag 8 — DefaultRipple) on the issuer wallet |
{
"success": true,
"bond": {
"id": "a1b2c3d4e5f67890",
"status": "active",
"opensAt": "2026-04-01T00:00:00Z",
"closesAt": "2026-05-01T00:00:00Z",
"maturesAt": "2027-04-01T00:00:00Z",
"paymentCount": 4,
"bondPageUrl": "https://onrampdlt.io/bonds/a1b2c3d4e5f67890"
}
}
POST /api/bonds/:id/subscribe
Record an investor subscription after setting a TrustLine and sending XRP payment. Auth required — unauthenticated calls return 401. The subscriber must have at least an Investor tier account ($49/yr), or any paid issuer plan (Starter+). Free accounts return 403. The API verifies the payment transaction on XRPL before recording the subscription and populates the user_id FK on the subscription record.
| Field | Type | Description |
|---|---|---|
| holderAddressreq | string | Investor's XRPL r-address |
| unitsreq | number | Number of bond units to subscribe |
| paymentTxHashreq | string | XRPL transaction hash of the XRP payment to the issuer |
{
"success": true,
"bondId": "a1b2c3d4e5f67890",
"holderAddress": "rXXX...",
"units": 5,
"xrpInvested": 500,
"xrpRewardExpected": "25.0000 XRP",
"message": "Subscribed to 5 unit(s). The issuer will send you tokens and reward."
}
GET /api/bonds/:id/xaman/:step
Generate Xaman-ready transaction payloads for issuer operations. Returns the raw txjson to sign in Xaman. Auth required — only the bond issuer can access these.
| Step | Description |
|---|---|
| account-set | AccountSet tx to enable DefaultRipple on the issuer wallet (required once before issuing) |
| trust-set-template | TrustSet template to share with investors so they can receive bond tokens |
| distribute-reward | Batch XRP Payment txs to distribute early-investor rewards to all holders |
| send-tokens | Batch Payment txs to deliver bond tokens to all subscribers |
{
"step": "send-tokens",
"instructions": "Sign each transaction in Xaman to deliver SRAB bond tokens to investors",
"count": 12,
"transactions": [
{
"holderAddress": "rXXX...",
"units": 5,
"txjson": {
"TransactionType": "Payment",
"Account": "rISSUER...",
"Destination": "rXXX...",
"Amount": { "currency": "5352414200...", "issuer": "rISSUER...", "value": "5" }
}
}
]
}
Billing
Billing is powered by Stripe. Subscriptions upgrade users from the free tier to Pro or Business. All billing management is handled through the Stripe Customer Portal.
POST /api/subscribe
Create a Stripe Checkout session for upgrading to a paid tier. Returns a checkout URL to redirect the user.
| Field | Type | Description |
|---|---|---|
| tieropt | string | investor, starter, pro (default), or business |
{ "checkoutUrl": "https://checkout.stripe.com/...", "sessionId": "cs_live_..." }
Redirect the user to checkoutUrl. On success, Stripe redirects to /dashboard.html?upgrade=success. On cancel, to /dashboard.html?upgrade=cancelled.
GET /api/manage-billing
Redirect the authenticated user to their Stripe Customer Portal to manage their subscription, payment method, or view invoices. Returns a 302 redirect.
On success: 302 redirect to the Stripe billing portal.
If no active subscription: 302 redirect to /dashboard.html?billing=no-subscription.
POST /api/stripe/webhook
Stripe webhook receiver. Handles subscription lifecycle events from Stripe and updates user tier accordingly. This endpoint is called by Stripe — not by API clients directly. All events are verified against the STRIPE_WEBHOOK_SECRET before processing. STRIPE_WEBHOOK_SECRET is required — the Worker hard fails at startup if absent.
Stripe signs every webhook request with a Stripe-Signature header. The platform verifies this signature using STRIPE_WEBHOOK_SECRET before processing any event. Unverified requests are rejected with HTTP 400.
| Event | Effect |
|---|---|
| checkout.session.completed | Upgrades user to the subscribed tier (investor, starter, pro, or business) |
| customer.subscription.deleted | Downgrades user to free tier |
| customer.subscription.updated | Updates user tier to reflect plan change |
Security
Authentication & Sessions
- JWT (HS256): Tokens are signed with a server-held secret. Expiry is 30 days, but tokens are validated server-side on every request — a logged-out token is immediately invalid regardless of expiry.
- Password hashing: PBKDF2/SHA-256 via Web Crypto API. Plaintext passwords are never stored, logged, or transmitted after the point of hashing.
- User enumeration protection: Login returns HTTP 401 with a generic message for both email-not-found and wrong-password. Prevents attackers from confirming whether an email is registered.
- JWT revocation: Logout stores the token in a Cloudflare KV revocation list with TTL equal to the token's remaining validity period. All subsequent requests — Bearer header or cookie — are checked against this blacklist before JWT signature verification. Logged-out tokens are immediately and permanently rejected regardless of expiry.
Transport & CORS
- HTTPS enforced via Cloudflare. All traffic is TLS-encrypted at the edge.
- CORS origins:
onrampdlt.io,www.onrampdlt.io,onrampdlt.com,www.onrampdlt.comonly. Credentials permitted viaAccess-Control-Allow-Credentials: true. - Cookie flags:
auth_tokenis set withHttpOnly; Secure; SameSite=None. HttpOnly prevents JavaScript access. Secure restricts to HTTPS.
Non-Custodial Architecture
- Private keys: Never requested, stored, transmitted, or accessible by the platform. All XRPL signing is performed client-side through Xaman.
- Transaction flow: The API constructs
txjsonand returns it. The user signs in Xaman. The signed transaction is broadcast to the XRPL directly from the client — the platform never submits transactions on behalf of users. - Fund custody: XRP subscription payments flow directly between investor and issuer XRPL wallets. The platform does not hold or intermediate these payments.
Payment Verification
Subscription payments (POST /api/bonds/:id/subscribe) are verified against the XRPL before any holding is recorded. The check confirms: transaction type is Payment, destination matches the bond issuer address, sender matches holderAddress, result is tesSUCCESS, and XRP amount is within 2% tolerance of expected. The tolerance prevents minor rounding discrepancies from blocking legitimate subscriptions.
Infrastructure
- Runs as a Cloudflare Worker at the edge — no traditional application server.
- Data stored in Cloudflare D1 (SQLite) and Cloudflare KV. No third-party databases hold user data outside Cloudflare and Stripe.
- Cloudflare Turnstile bot protection on auth endpoints when configured.
Required Secrets
The following environment secrets must be set on the Cloudflare Worker. The Worker performs a hard startup failure if any required secret is absent — it will not serve requests in a degraded state.
| Secret | Required | Notes |
|---|---|---|
| JWT_SECRET | Required | Used to sign and verify all JWTs. Must be cryptographically strong. |
| STRIPE_SECRET_KEY | Required | Stripe API key for creating checkout sessions and customer portal links. |
| STRIPE_WEBHOOK_SECRET | Required | Stripe webhook signing secret. Used to verify the authenticity of all incoming Stripe webhook events. The Worker performs a hard fail at startup if this secret is absent — it will not process any requests without it. Set this in the Cloudflare Workers dashboard under Settings > Variables & Secrets. |
| XAMAN_API_KEY | Required for Xaman flows | XAMAN API key for generating QR payloads and polling signing status. |
| XAMAN_API_SECRET | Required for Xaman flows | XAMAN API secret paired with the API key. |
STRIPE_WEBHOOK_SECRET is not set. All Stripe webhook events are verified using this secret before any processing occurs.Error Codes
All error responses return JSON with an error field. The HTTP status indicates the category of failure.
| Status | Category | Common Causes |
|---|---|---|
| 400 | Bad Request | Missing required fields, invalid values, business rule violations (duplicate wallet, wrong bond status), payment verification failure. |
| 401 | Unauthorized | No JWT, expired JWT, invalid signature, session deleted. |
| 403 | Forbidden | Authenticated but not permitted — free tier trying to issue bonds, modifying another issuer's bond. |
| 404 | Not Found | Bond ID does not exist. Route does not exist. |
| 429 | Too Many Requests | IP rate limit exceeded on login or signup. Retry after window expires. |
| 500 | Internal Server Error | Unexpected API failure. Logged server-side. Retry once — if persisting, contact support. |
{ "error": "Human-readable description of what went wrong" }
KYC & Compliance
Platform KYC Role
OnRampDLT is a technology platform, not a broker-dealer, investment adviser, placement agent, or transfer agent. The platform provides software tools for issuers to create and manage token and bond offerings on the XRP Ledger. It does not act as an intermediary in securities transactions, does not hold investor funds, and does not make suitability determinations.
What OnRampDLT Verifies vs. Does Not Verify
| Verification | Status | Notes |
|---|---|---|
| Email address uniqueness | ✓ Platform-verified | Must be unique for account creation |
| XRPL r-address format | ✓ Platform-verified | Classic r-address format validated at registration |
| Issuer wallet XRPL balance | ✓ Platform-verified | Live balance checked via XRPL at bond activation |
| Investor XRP payment on-chain | ✓ Platform-verified | Payment tx verified against XRPL before recording subscription |
| Accredited investor status | ✗ Not verified | Issuer responsibility |
| Investor identity (KYC/AML) | ✗ Not verified | Issuer responsibility |
| Investor suitability | ✗ Not verified | Issuer responsibility |
| Form D filing | ✗ Not handled | Issuer responsibility — file at SEC EDGAR |
| State blue sky compliance | ✗ Not handled | Issuer responsibility |
Reg D Requirements by Offering Type
| Requirement | Rule 506(b) | Rule 506(c) |
|---|---|---|
| Investor eligibility | Up to 35 non-accredited sophisticated investors; unlimited accredited investors | Accredited investors only |
| General solicitation | Prohibited | Permitted |
| Accredited investor verification | Self-certification acceptable | Issuer must take reasonable steps to verify — third-party letters, tax returns, bank statements, or verification service |
| Form D filing deadline | 15 calendar days after first sale | 15 calendar days after first sale |
| Resale restrictions | Restricted securities — Rule 144 holding period (1 year for non-reporting issuers) | Same |
| Bad actor check | Rule 506(d) required | Rule 506(d) required |
Legal & Compliance
Legal Overview
This section describes the regulatory framework, issuer obligations, and investor protections applicable to offerings conducted using this platform. Issuers and their counsel should read this section in full before conducting any offering.
Platform Role
OnRampDLT is a software-as-a-service technology platform. Its functions are limited to providing software tools, recording offering metadata, generating Xaman-compatible XRPL transaction payloads for client-side signing, and verifying that XRPL payment transactions exist on-chain.
OnRampDLT does not:
- Act as a broker-dealer, placement agent, investment adviser, or transfer agent.
- Custody, control, or transmit investor or issuer funds. All XRP flows directly between XRPL wallets without platform intermediation.
- Control private keys. All XRPL signing is performed by the user through Xaman.
- Determine investor suitability, accredited investor status, or eligibility.
- Provide investment advice or recommendations regarding any offering.
- Guarantee the performance, repayment, or legitimacy of any offering.
- File regulatory documents (Form D, state notices) on behalf of issuers.
Regulation D Requirements
Rule 506(b) — Private Offerings
- No general solicitation or advertising. Offering must be made to investors with whom the issuer has a pre-existing substantive relationship.
- Up to 35 non-accredited sophisticated investors; unlimited accredited investors. Non-accredited investors must receive disclosure documents equivalent to a registered offering.
- Form D must be filed with the SEC within 15 days of first sale.
Rule 506(c) — General Solicitation Permitted
- General solicitation and advertising expressly permitted.
- All investors must be accredited. Issuers must take reasonable steps to verify — self-certification alone is not sufficient. Acceptable methods: written confirmation from registered broker-dealer, SEC-registered investment adviser, licensed attorney, or CPA; review of tax returns, W-2s, or financial statements; or third-party verification service.
- Form D must be filed within 15 days of first sale. An amendment noting general solicitation must be filed before or concurrent with any solicitation.
Form D
File electronically through SEC EDGAR within 15 calendar days of first sale. Failure to file may result in loss of the exemption. The platform does not file Form D on behalf of issuers.
Resale Restrictions
Securities sold under Reg D are restricted securities under Rule 144. Investors may not resell without: a satisfied holding period (1 year for non-reporting issuers), and satisfaction of Rule 144 conditions, or a separate registration or exemption. Issuers must ensure investors receive written notice. Subscription agreements should include a restrictive legend.
Bad Actor Disqualification (Rule 506(d))
Issuers must verify no "covered person" — the issuer entity, its directors, officers, 20%+ beneficial owners, placement agents and affiliates — is subject to a disqualifying event (SEC orders, criminal convictions, court injunctions, regulatory sanctions). Diligence must be performed before first sale and periodically thereafter. The platform does not perform this check.
Investor Protections & Disclosures
- Risk of loss: Investment in private bond offerings involves substantial risk, including total loss of principal. These are illiquid, unregistered securities.
- No secondary market: There is no guaranteed secondary market for bond tokens. Investors should expect to hold until maturity.
- No SIPC/FDIC protection: Investments are not bank deposits and are not insured by the FDIC, SIPC, or any government agency.
- Issuer default risk: The platform does not guarantee any issuer's ability to make payments or repay principal and has no recourse mechanism against defaulting issuers.
- Technology risk: XRPL transactions are irreversible once confirmed. Loss of wallet access, incorrect addresses, or XRPL network events are outside the platform's control.
- Regulatory risk: The regulatory treatment of blockchain-based securities is evolving. Changes in law or interpretation could affect the legality or operation of offerings.
Data & Privacy
| Data | Stored | Notes |
|---|---|---|
| Email address | Yes — Cloudflare D1 | Required for account |
| Password | Hash only (PBKDF2/SHA-256) | Plaintext never stored |
| XRPL wallet address (public) | Yes — D1 + KV | Public on-chain data |
| Bond offering parameters | Yes — KV store | Duration of bond |
| Investor XRPL addresses + holdings | Yes — KV store | Associated with bond record |
| Private keys / seed phrases | Never | Non-custodial by design |
| Payment card data | Never | Stripe handles all payment processing |
| Government-issued ID | Not collected | Platform does not perform KYC |
| Session tokens (JWT) | Yes — D1, 30-day TTL | Deleted on logout |
| Stripe customer ID | Yes — D1 | For billing management |
Glossary
Changelog
- Added: POST /api/auth/resend-verification — resend email verification link, rate limited 3/hour per email, no auth required
- Updated: POST /api/auth/login now returns HTTP 403 with
{ email_unverified: true, email }when email is not verified - Updated: PUT /api/bonds/:id/activate now requires
accountsetTxhashin body — the signed AccountSet transaction hash (SetFlag 8 — DefaultRipple) - Added: POST /api/stripe/webhook documented — STRIPE_WEBHOOK_SECRET is now required (hard fail at startup if absent)
- Added: Investor tier ($49/year) documented across all tier references — positioned between free and starter
- Added: Required Secrets table in Security section documenting STRIPE_WEBHOOK_SECRET enforcement
- Added: POST /api/subscribe now accepts
investorandstarteras valid tier values
- Fixed: GET /api/auth/me was returning 401 for all email-auth users — SQL query referenced columns not present in live DB
- Fixed: JWT logout now performs server-side revocation via Cloudflare KV blacklist — Bearer tokens are rejected immediately after logout
- Fixed: GET /api/tokens was publicly accessible with no auth — auth gate added
- Fixed: POST /api/tokens/xaman/payload had no auth or tier gate — now requires Starter tier
- Fixed: POST /api/auth/signup returned 500 on empty body — now returns 400 with descriptive error
- Fixed: POST /api/auth/signup accepted invalid email formats — email regex validation added
- Added: POST /api/issuer/profile route (was PUT-only, now both POST and PUT perform upsert)
- Added: GET /api/tier/features endpoint returning all feature flags for current tier
- Corrected: API base URL is api.onrampdlt.com (not api.onrampdlt.io)
- Documented: Crossmark and GemWallet support — both verified as valid wallet_type values in the wallets API
- Documented: KYC self-certify endpoint with correct field names
- Documented: XAMAN wallet auth flow (payload + status polling)
- Documented: Issuer profile API (GET/POST/PUT)
- Documented: Wallet management API with tier limits
- Initial public API release
- Email/password auth with JWT session management and PBKDF2/SHA-256 password hashing
- Issuer wallet registration with live XRPL balance checking
- Token issuance (POST /api/tokens) with tier enforcement — Starter required
- XAMAN QR-based token trust-set flow (POST /api/tokens/xaman/payload)
- Bond platform — full lifecycle: create, activate, subscribe, Xaman signing payloads
- Reg D tier gating — 506(b) requires Pro, 506(c) requires Business
- On-chain payment verification with 2% tolerance for subscription recording
- Bond interest payment schedule generation at activation
- Early-investor XRP reward distribution via Xaman batch signing
- Stripe billing — Starter, Pro, and Business tier subscriptions with Customer Portal
- Automatic tier upgrade/downgrade via Stripe webhook events
- KYC self-certification (506(b)) and Stripe Identity verification (506(c))
- Team member management for Business/Enterprise accounts
- Admin backend for user management, email broadcasts, and audit log review
Versioning Policy
The API follows semantic versioning. Breaking changes (removed fields, changed response structures, deleted endpoints) will increment the major version with a minimum 90-day deprecation notice. Additive changes (new endpoints, new optional fields) are non-breaking. All endpoints are currently under v1 — no version prefix is required in the URL path.