In development, browse the interactive API docs at GET /api/docs.
- Campaigns define a default settlement asset via
campaigns.asset_type(USDCorXLM). - Contributors can pay using
send_asset. - If
send_asset !== campaign.asset_type, the backend uses StellarpathPaymentStrictReceiveso the campaign receives the exact requestedamountin its settlement asset. - Conversion path discovery uses Stellar Horizon
strictReceivePathsand applies a5%slippage buffer when computingsendMax. - Additional credit assets can be enabled through
STELLAR_EXTRA_ASSETSin.envas a JSON object ({"CODE":"ISSUER"}).
Authenticated. Returns the current profile, including email_verified (boolean), kyc_status (unverified, pending, verified, rejected) and kyc_completed_at.
Public. Validates a verification token and marks the associated user's email as verified.
Query params:
token(required): The UUID token sent via email.
Returns 200 on success or 400/410 if the token is invalid or expired (> 24 hours).
Authenticated. Generates a new verification token and sends a new email. Rate-limited to 3 requests per hour per user.
Authenticated. Creates a hosted KYC session with the configured provider and marks the user pending.
Response includes redirect_url or session_token, plus the updated user. Persona is used when KYC_PROVIDER=persona, PERSONA_API_KEY, and PERSONA_TEMPLATE_ID are configured; otherwise local development returns a dev redirect URL.
KYC provider callback. Updates the matched user to verified or rejected from provider status. The campaign creation gate is controlled by KYC_REQUIRED_FOR_CAMPAIGNS and defaults to enabled; set it to false for testnet/dev.
Authenticated creator/admin endpoint. The user must have email_verified=true. Additionally, when KYC_REQUIRED_FOR_CAMPAIGNS is not false, the user must have kyc_status=verified; otherwise the API returns 403 with code=KYC_REQUIRED or code=EMAIL_NOT_VERIFIED.
Get a DEX quote before submitting a conversion contribution.
Query params:
send_asset(required):XLMorUSDCdest_asset(required):XLMorUSDCdest_amount(required): amount the campaign should receive
Success response (200):
{
"send_asset": "XLM",
"dest_asset": "USDC",
"dest_amount": "9",
"quoted_source_amount": "10.0000000",
"max_send_amount": "10.5000000",
"estimated_rate": "0.900000000000000",
"path": ["AQUA"],
"path_count": 3
}Errors:
400missing/invalid params404no path found on Stellar DEX
Submit a contribution through the existing custodial wallet flow (direct payment or path payment).
Body:
campaign_id(required)amount(required): amount the campaign must receive in campaign assetsend_asset(required):XLMorUSDC
Success response (202):
{
"tx_hash": "c8d6...",
"message": "Transaction submitted",
"conversion_quote": {
"send_asset": "XLM",
"campaign_asset": "USDC",
"campaign_amount": "4.5000000",
"quoted_source_amount": "5.0000000",
"max_send_amount": "5.2500000",
"path": []
}
}conversion_quote is null for direct same-asset contributions.
Errors:
400missing fields / unsupported assets404campaign not found or not active422no conversion path found for requested asset pair
Prepare an unsigned Stellar transaction for a Freighter contribution without submitting it.
Body:
campaign_id(required)amount(required): amount the campaign must receive in campaign assetsend_asset(required):XLMorUSDCsender_public_key(required): contributor wallet public key from Freighter
Success response (200):
{
"unsigned_xdr": "AAAAAgAAA...",
"prepare_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"conversion_quote": null,
"sender_public_key": "G...",
"network_passphrase": "Test SDF Network ; September 2015",
"network_name": "TESTNET"
}prepare_token is short-lived and must be returned with the signed XDR to /api/contributions/submit-signed.
Errors:
400missing fields / invalid Stellar public key / unsupported asset404campaign not found or not active422no conversion path found for requested asset pair
Submit a Freighter-signed contribution transaction after backend validation.
Body:
prepare_token(required): opaque token returned by/preparesigned_xdr(required): transaction signed in Freighter
Success response (202):
{
"tx_hash": "c8d6...",
"stellar_transaction_id": "0f3f...",
"message": "Transaction submitted",
"conversion_quote": null
}Validation performed before submission:
- signed transaction source account must match
sender_public_key - signed transaction body hash must match the prepared unsigned XDR exactly
- signed transaction must contain a valid signature for the contributor public key
Errors:
400missing fields / invalid prepare token403prepare token belongs to a different authenticated user422signed XDR does not match the prepared transaction502Stellar rejected the signed transaction
Returns the configured fiat on-ramp anchors and the CrowdPay asset codes currently enabled on Stellar.
Authenticated. Starts a SEP-24 hosted deposit session for the chosen anchor after backend SEP-10 authentication.
Body:
campaign_id(required)amount(required): amount the campaign should ultimately receiveanchor_id(required): anchor identifier from/api/anchor/info
Success response includes:
- anchor session
id interactive_urlanchor_transaction_idanchor_asset/anchor_amountconversion_quotewhen the eventual contribution needs a path payment
Authenticated. Polls the anchor transaction, updates the local anchor session, and once the deposit completes automatically submits the normal Stellar contribution from the user’s custodial wallet.
Anchor deposit support requires a configured backend anchor signing wallet and an enabled anchor.
Set ANCHOR_WALLET_HOME_DOMAIN and ANCHOR_WALLET_SIGNING_SECRET in the backend environment.
MoneyGram anchor support is enabled by default unless ANCHOR_MONEYGRAM_ENABLED=false.
Use ANCHOR_MONEYGRAM_ENV=sandbox|preview|production to select the MoneyGram deployment.
For a custom anchor, configure ANCHOR_CUSTOM_ID, ANCHOR_CUSTOM_NAME, ANCHOR_CUSTOM_HOME_DOMAIN, ANCHOR_CUSTOM_WEB_AUTH_ENDPOINT, ANCHOR_CUSTOM_SEP24_ENDPOINT, ANCHOR_CUSTOM_SIGNING_KEY, ANCHOR_CUSTOM_ASSET_CODE, and ANCHOR_CUSTOM_ASSET_ISSUER.
Fetch indexed contributions with conversion audit fields.
Success response (200):
[
{
"id": "0f3f...",
"sender_public_key": "G...",
"amount": "4.5000000",
"asset": "USDC",
"payment_type": "path_payment_strict_receive",
"source_amount": "4.9973210",
"source_asset": "XLM",
"conversion_rate": "0.900482150000000",
"path": ["AQUA"],
"tx_hash": "c8d6...",
"created_at": "2026-04-23T08:13:34.392Z"
}
]Returns whether the authenticated user may perform platform signing/rejection (can_approve_platform). If PLATFORM_APPROVER_USER_ID is set in the backend environment, only that user’s JWT subject matches; otherwise (dev only) any authenticated user may act as platform for API calls.
Create a pending withdrawal request (creator only). Verifies multisig on the campaign wallet, ensures the campaign is active or funded, and rejects if another pending withdrawal already exists for the same campaign.
Body:
campaign_id(required)destination_key(required)amount(required)
Returns 201 with withdrawal request (creator_signed=false, platform_signed=false, status=pending).
Appends an audit row to withdrawal_approval_events with action requested.
Errors:
403not the campaign creator409campaign status not eligible, or duplicate pending withdrawal422multisig configuration invalid
Creator approval step. Signs withdrawal XDR using creator custodial key and marks creator_signed=true.
Errors:
403caller is not campaign creator409request no longer pending, campaign status notactive/funded, or already creator-approved
Logs creator_signed in withdrawal_approval_events.
Platform approval/finalization step. Signs with platform key, validates dual-signature presence, and submits to Stellar.
Errors:
403caller cannot perform platform signature (seePLATFORM_APPROVER_USER_ID)409creator approval missing, campaign status not eligible, or request not pending422insufficient signatures in XDR502Stellar rejected the transaction — request is markedfailedandsubmit_failedis logged
Success:
- marks request as
status=submitted - stores Stellar
tx_hash - logs
platform_signedinwithdrawal_approval_events
Creator-only. Cancels a pending request before creator signature (creator_signed=false). Sets status=denied and stores optional reason in denial_reason. Logs creator_cancelled.
Platform-only (same rules as platform approve). Rejects a pending request after creator has signed and before platform signature. Sets status=denied. Body optional: { "reason": "..." }. Logs platform_rejected.
List withdrawal requests for a campaign (denial_reason included when denied). Authorized for campaign creator or configured platform approver only (others receive 403).
Immutable audit timeline for one withdrawal: action, actor_user_id, note, metadata, created_at. Same authorization as the campaign list endpoint.
List milestones for a campaign in display order.
Success response (200):
[
{
"id": "0f3f...",
"campaign_id": "4db6...",
"title": "Prototype delivery",
"description": "Ship the first production-ready prototype to pilot users.",
"release_percentage": "25.0000",
"sort_order": 0,
"status": "pending",
"evidence_url": null,
"destination_key": null,
"review_note": null,
"created_at": "2026-04-26T08:13:34.392Z",
"completed_at": null,
"approved_at": null,
"released_at": null
}
]Creator-only. Submit milestone completion evidence and the payout destination for that release.
Body:
evidence_url(required)destination_key(required): Stellar public key that will receive the approved release
Errors:
403caller is not the campaign creator409campaign is not yet in a releaseable state, or milestone is already released400destination key is invalid
Platform-only. Reviews the submitted milestone, signs the escrow withdrawal using the existing dual-signature flow, submits it to Stellar, records the withdrawal, and advances campaign status to in_progress or completed.
Body:
reason(optional): review note stored with the milestone and audit trail
Errors:
403caller cannot perform platform approval409evidence or payout destination is missing, campaign status is notfunded/in_progress, or release already exists422dual signature requirements were not met502Stellar rejected the release transaction
Platform-only. Rejects a submitted milestone and stores a required review note.
Body:
reason(required)
-
Every indexed contribution stores:
payment_type(paymentvspath_payment_strict_receive)- destination settlement
amountandasset - conversion source
source_amountandsource_asset(when applicable) conversion_rate(destination_amount / source_amount)- conversion
pathas JSON - immutable Stellar
tx_hash
-
This enables independent reconciliation against Horizon payment records by
tx_hash. -
Manual fund releases append rows to
withdrawal_approval_events(requested,creator_signed,platform_signed,creator_cancelled,platform_rejected,submit_failed) with optionalnoteandmetadataJSON for audit and manual review. -
Milestone-based releases reuse the same multisig withdrawal machinery, but the release is triggered from platform approval after the creator has submitted evidence and a payout destination.
-
Anchor-assisted contributions persist
anchor_idandanchor_transaction_idon the finalcontributionsrow for support and reconciliation.
Public JSON snapshot for operations: Horizon cursor row per active campaign wallet (from ledger_stream_cursors), in-process SSE stream state (connected, reconnecting, error, not_connected), last message time, reconnect attempt count, and stale_stream_no_messages_15m when a supposedly connected stream has had no SSE traffic for 15 minutes.
The backend also logs a warning every 5 minutes if any wallet is in that stale state.
node --test src/**/*.test.js includes route coverage for:
- quote endpoint success
- quote endpoint no-path behavior
- direct payment path for
XLM -> XLM - direct payment path for
USDC -> USDC - conversion path payment for
XLM -> USDC - conversion path payment for
USDC -> XLM - withdrawal request creation with multisig validation
- withdrawal creator/platform approval flow
- withdrawal denial paths (missing creator approval, insufficient signatures)