marketplace-doc
    • Data Ingestion
    • Errors
    • Introduction
    • Loan API & Deduction Lifecycle
    • Getting Started
    • OAuth
    • Webhooks
    • Embedded Journey
    • OpenSylo Marketplace Integration API
      • OAuth 2.0
        • Start OAuth authorization
        • Exchange authorization code or refresh token
        • Revoke a token
        • Discover OAuth capabilities
      • Data Ingestion
        • Submit single merchant data
        • Submit bulk merchant data
        • Poll batch processing status
        • Get merchant credit score
        • Data ingestion health check
      • Sales & Events
        • Submit a sales event
        • Submit a repayment event
        • Submit an account flag
      • Loan API
        • Get active loans for a merchant
        • Get loan status
        • Validate deduction amounts
        • Bulk loan status
      • Inbound Webhooks
        • Repayment events
        • Settlement events
      • Outbound Webhooks
        • loan.approved
        • loan.disbursed
        • loan.repayment_updated
        • loan.nearly_complete
        • loan.completed
        • loan.defaulted
        • merchant.created
        • kyc.submitted
        • kyc.approved
        • kyc.rejected
        • funding_request.created
        • funding_request.fulfilled
        • funding_request.rejected
      • Embedded Journey (Marketplace API)
        • Create (or fetch) a merchant
        • Get merchant status (KYC, credit score, funding requests)
        • Update business KYC information
        • Add directors (bulk)
        • Attach a KYC document
        • Submit KYC for review
        • Submit sales data for credit scoring
        • Create a funding request
        • Mint an embed token for the hosted journey
      • Schemas
        • TokenRequest
        • TokenResponse
        • OAuthError
        • ClientMetadataResponse
        • MerchantIdentity
        • SalesPerformance
        • RevenueConsistency
        • FulfillmentMetrics
        • PayoutCashFlow
        • PlatformDependency
        • HistoricalCredit
        • BehavioralRisk
        • MonthlyHistoryEntry
        • MerchantDataRequest
        • CreditScore
        • LoanEligibilityResult
        • MerchantDataResponse
        • BulkMerchantDataRequest
        • AsyncBulkProcessingResponse
        • BatchStatusResponse
        • SalesEventRequest
        • RepaymentEventRequest
        • AccountFlagRequest
        • MerchantInfo
        • LenderInfo
        • RepaymentInfo
        • LoanTermsInfo
        • ActiveLoan
        • ActiveLoansSummary
        • ActiveLoansResponse
        • LoanStatusRepayment
        • LoanStatusResponse
        • ValidateDeductionsRequest
        • DeductionItem
        • DeductionSummary
        • ValidateDeductionsResponse
        • BulkLoanStatusRequest
        • BulkLoanStatusItem
        • BulkStatusSummary
        • BulkLoanStatusResponse
        • WebhookMarketplace
        • WebhookMerchant
        • WebhookLoan
        • DeductionDetails
        • SourceTransaction
        • LoanBalance
        • RepaymentDeductedWebhook
        • FailureDetails
        • RepaymentFailedWebhook
        • ReversalDetails
        • RepaymentReversedWebhook
        • SettlementDetails
        • SettlementSummaryByStatus
        • SettlementSummary
        • SettlementLoanIncluded
        • BankTransferDetails
        • SettlementCreatedWebhook
        • SettlementTransferredWebhook
        • TransferDetails
        • WebhookAckResponse
        • OutboundLoan
        • OutboundMerchant
        • OutboundLender
        • LoanTerms
        • RepaymentTerms
        • ScheduleInstallment
        • LoanApprovedPayload
        • DisbursementDetails
        • DisbursedRepayment
        • LoanDisbursedPayload
        • ChangeDetail
        • LoanRepaymentUpdatedPayload
        • RepaymentStatus
        • NearlyCompleteRecommendation
        • LoanNearlyCompletePayload
        • CompletionSummary
        • LoanCompletedPayload
        • DefaultDetails
        • CollectionInstructions
        • LoanDefaultedPayload
        • ApiError
        • RateLimitError
        • EmbedCreateMerchantRequest
        • EmbedCreateMerchantResponse
        • EmbedBusinessKyc
        • EmbedDirectorInfo
        • EmbedBulkDirectors
        • EmbedDirectorListItem
        • EmbedDocumentUpload
        • EmbedSalesDataAccepted
        • EmbedCreateFundingRequest
        • EmbedFundingRequestIntent
        • EmbedMerchantStatusResponse
        • EmbedTokenResponse
        • MarketplaceMerchantRef
        • MerchantCreatedPayload
        • KycStatusPayload
        • FundingRequestEventPayload
    • OpenSylo Marketplace API
      • OAuth 2.0
        • Start OAuth authorization
        • Exchange code or refresh token
        • Revoke a token
        • OAuth discovery / client metadata
      • Data Ingestion
        • Submit single merchant data
        • Submit bulk merchant data
        • Get merchant credit score
        • Integration health check
      • Loan API
        • Get active loans for a merchant
        • Get loan status
        • Validate deduction amounts
        • Bulk loan status check
      • Inbound Webhooks
        • Send repayment webhook
        • Send settlement webhook
      • Schemas
        • TokenRequest
        • TokenResponse
        • RevokeRequest
        • ClientMetadataResponse
        • MerchantIdentity
        • SalesPerformance
        • RevenueConsistency
        • FulfillmentMetrics
        • PayoutCashFlow
        • PlatformDependency
        • HistoricalCredit
        • BehavioralRisk
        • MerchantDataRequest
        • ScoreBreakdown
        • CreditScore
        • MerchantDataResponse
        • BulkMerchantDataRequest
        • BulkMerchantDataResponse
        • CreditScoreResponse
        • HealthResponse
        • ActiveLoansResponse
        • LoanStatusResponse
        • ValidateDeductionsRequest
        • ValidateDeductionsResponse
        • BulkLoanStatusRequest
        • BulkLoanStatusResponse
        • RepaymentWebhookRequest
        • SettlementWebhookRequest
        • WebhookAckResponse
        • OAuthError
        • ApiError

    Embedded Journey

    Overview#

    The embedded application journey lets your marketplace onboard merchants for funding without ever sending them to OpenSylo. Your backend drives onboarding through the Marketplace API (authenticated with your secret key), and your dashboard renders the hosted application journey inside an iframe using the OpenSylo Embed SDK — no redirect, no OpenSylo login for the merchant.
    There are two complementary surfaces:
    SurfaceAuthUsed byPurpose
    Marketplace API (/api/v1/marketplace-api/*)Secret key (sk_test_* / sk_live_*)Your backendCreate merchants, prefill KYC, ingest sales data, mint embed tokens, create funding requests, poll status
    Embedded journey (iframe via Embed SDK)Short-lived embed tokenThe merchant, in your dashboardBusiness info, directors, document uploads, KYC submission, funding requests, offer acceptance, payout setup
    You choose the split: collect everything via API and use the iframe only for review/submission, or create the merchant via API and let the iframe collect everything. Both paths write to the same KYC record.
    The iframe itself talks to OpenSylo's /api/v1/embed/journey/* endpoints using the embed token. Those endpoints are called by the hosted journey UI, not by your integration, so they are not part of your API surface — you only mint the token.

    Prerequisites#

    RequirementWhere
    Marketplace account and dashboard accessOpenSylo admin invitation
    Secret keys (sk_test_*, sk_live_*)Marketplace dashboard → Settings → API Credentials
    Webhook endpoint registered + apiSecret for signature verificationMarketplace dashboard → Settings → API Credentials
    Allowed embedding domains registeredMarketplace dashboard → Settings → API Credentials → Allowed Embedding Domains (see Allowed embedding domains)

    Authentication#

    Every Marketplace API request must carry your secret key in one of two ways:
    X-OpenSylo-Secret-Key: sk_test_xxx
    or
    Authorization: Bearer sk_test_xxx
    The key prefix selects the environment automatically: sk_test_* operates on test data, sk_live_* on production data. No separate base URL.
    sk_live_* keys are rejected with 403 until your marketplace has been switched live by OpenSylo.
    Keep secret keys on your backend only. Never ship a secret key to the browser — the only credential that ever reaches your frontend is the short-lived embed token.
    Base URL: https://api.opensylo.com (same as the rest of the platform).

    Step 1 — Create the merchant#

    Endpoint: POST /api/v1/marketplace-api/merchants
    Creates (or returns) the OpenSylo merchant for one of your customers. The call is idempotent on thirdPartyCustomerId: repeating it with the same ID returns the existing merchant instead of creating a duplicate, so it is safe to call on every "Get funding" click.
    FieldRequiredNotes
    thirdPartyCustomerIdyesYour stable ID for the merchant (max 255). Idempotency key, echoed in every webhook.
    businessNameyesMax 150
    emailyesValid email
    phoneNumberyesE.164, e.g. +2348012345678
    firstName, lastNamenoPrimary contact (max 50 each)
    businessTypenoe.g. LIMITED_LIABILITY_COMPANY, SOLE_PROPRIETORSHIP
    industrynoe.g. FOOD
    businessCategorynoFree text, max 50
    externalReferencenoStored on the connection, max 255

    curl Example#

    Response#

    {
      "merchantId": "a1b2c3d4-...",
      "connectionId": "c9d8e7f6-...",
      "thirdPartyCustomerId": "mkt_merchant_8421",
      "created": true
    }
    Store merchantId alongside your own customer record. Every other endpoint takes :ref, which may be either the thirdPartyCustomerId or the merchantId.
    After creation you receive a merchant.created webhook.

    Step 2 — Submit sales data#

    Endpoint: POST /api/v1/marketplace-api/merchants/:ref/sales-data
    Credit scoring is driven by the performance data you hold about the merchant. Submit it as early as possible — a funding request cannot be fulfilled until a credit score exists.
    The payload is the standard marketplace data-capture structure (merchantIdentity, salesPerformance, revenueConsistency, fulfillmentMetrics, payoutCashFlow, platformDependency, historicalCredit, behavioralRisk, and optional monthlySalesHistory). See the Data Ingestion guide for the field-by-field reference — the body is the same; only the endpoint and authentication differ.

    Response#

    {
      "accepted": true,
      "status": "processing",
      "merchantId": "a1b2c3d4-...",
      "thirdPartyCustomerId": "mkt_merchant_8421",
      "submittedAt": "2026-06-10T10:00:00.123Z"
    }
    Notes:
    Ingestion is asynchronous — scoring typically completes within ~5 minutes. Poll GET /merchants/:ref and check for a populated creditScore.
    Include monthlySalesHistory (6 months, oldest→newest) to enable repayment-capacity-based loan eligibility.
    The first successful ingestion transitions the connection from PENDING to ACTIVE.

    Step 3 (optional) — Prefill KYC via API#

    Anything you prefill here appears pre-populated when the merchant opens the embedded journey, so they only review and complete what's missing. All three endpoints accept :ref = thirdPartyCustomerId or merchantId.

    Business information#

    Endpoint: PUT /api/v1/marketplace-api/merchants/:ref/business-info
    {
      "businessName": "Mama Cass Foods Ltd",
      "businessType": "LIMITED_LIABILITY_COMPANY",
      "operatingAddress": "15 Ikoyi Road, Victoria Island, Lagos",
      "registeredAddress": "15 Ikoyi Road, Victoria Island, Lagos",
      "registrationNumber": "RC123456",
      "taxNumber": "TIN0987654321",
      "dateOfIncorporation": "2022-05-10",
      "businessCategory": "Quick Service Restaurant",
      "businessEmail": "info@mamacass.example",
      "businessPhone": "+2341234567890",
      "website": "https://mamacass.example"
    }
    businessName, businessType, and operatingAddress are required; the rest are optional.

    Directors#

    Endpoint: POST /api/v1/marketplace-api/merchants/:ref/directors
    Takes a directors array. Each director:
    FieldRequiredValidation
    firstName, lastNameyes2–50 chars, letters/spaces/hyphens/apostrophes
    middleNamenoMax 50
    emailyesValid email
    phoneNumberyesE.164
    addressyesMax 500
    dateOfBirthyesISO 8601, in the past
    nationalityyesMax 50
    identityTypeyesBVN, NIN, PASSPORT, or DRIVERS_LICENSE
    identityNumberyesBVN/NIN: exactly 11 digits. Passport/licence: 5–20 alphanumeric
    positionnoCEO, CTO, CFO, COO, Director, Chairman, Secretary, Shareholder
    isPrimarynoMarks the primary contact
    BVN identities are verified automatically; other identity types go to manual review (verificationStatus: VERIFIED, PENDING, or MANUAL_REVIEW). Identity numbers are masked in all responses ("****4351").

    Documents#

    Endpoint: POST /api/v1/marketplace-api/merchants/:ref/documents
    {
      "documentType": "CAC_CERTIFICATE",
      "fileName": "cac_certificate.pdf",
      "fileKey": "merchants/{merchantId}/kyc/cac_certificate/...",
      "fileSize": "524288",
      "mimeType": "application/pdf"
    }
    documentType values include CAC_CERTIFICATE, TAX_CERTIFICATE, MEMORANDUM_AND_ARTICLES, BUSINESS_PERMIT, BANK_STATEMENT, FINANCIAL_STATEMENT, UTILITY_BILL, PROOF_OF_ADDRESS, ID_CARD, PASSPORT, DRIVERS_LICENSE, and director variants (DIRECTOR_ID, DIRECTOR_PASSPORT, DIRECTOR_BVN).
    The fileKey comes from OpenSylo's presigned-upload flow (request → upload to storage → confirm). In practice most partners let merchants upload documents inside the embedded journey, which handles presign/upload/confirm/attach automatically — prefer that unless you already hold the documents.

    Submit for review#

    Endpoint: POST /api/v1/marketplace-api/merchants/:ref/submit (empty body)
    Transitions KYC to SUBMITTED and queues it for review. You receive a kyc.submitted webhook immediately, then kyc.approved or kyc.rejected after review. The merchant can equally submit from inside the iframe — same effect.

    Step 4 — Embed the journey#

    4.1 Mint an embed token (backend)#

    Endpoint: POST /api/v1/marketplace-api/merchants/:ref/embed-tokens (empty body)
    {
      "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
      "expiresIn": 900,
      "environment": "test"
    }
    The token is a 15-minute JWT scoped to exactly one merchant (audience opensylo:embed). It cannot be used against any other OpenSylo API.
    environment mirrors the key you used (sk_test_* → test).
    Mint a fresh token each time the merchant opens the journey — don't cache it.
    Deliver it to your frontend over a secure channel (your authenticated session). Never put it in a shareable URL; the SDK passes it to the iframe in a URL fragment, which is not sent to any server.

    4.2 Render the journey (frontend)#

    Install from npm:
    Or load the versioned CDN bundle (exposes window.OpenSyloEmbed) with Subresource Integrity:
    Pin a version and use integrity as above. The mutable alias https://sdk.opensylo.com/v1/opensylo-embed.global.js auto-upgrades within v1 and therefore must be used without an integrity attribute.

    ApplicationJourney options#

    OptionTypeRequiredDescription
    tokenstringyesEmbed token from your backend
    containerstring | HTMLElementyesWhere to mount the iframe
    environment"test" | "live"noDefaults to "test"
    hoststringnoOrigin of the hosted journey. Defaults to https://embed.opensylo.com
    theme.primarystringnoBrand color, e.g. #ff5500
    theme.logostringnoLogo URL shown at the top of the journey
    theme.radiusstringnoCorner radius, e.g. 12px
    onReadyfunctionnoJourney mounted with a valid token
    onStepChangefunctionno{ step } — merchant advanced a step
    onCompletedfunctionno{ merchantId? } — application submitted
    onErrorfunctionno{ code?, message } — fatal error (e.g. expired token)
    The returned handle exposes iframe (the element) and destroy() (removes the iframe and detaches listeners).

    SDK events#

    The SDK and the journey communicate over postMessage with strict two-way origin checks — the SDK only accepts messages from the embed host, and the journey only posts to your registered origin.
    EventPayloadWhen
    ready–Journey mounted with a valid token
    step-change{ step }Merchant advances a step
    completed{ merchantId? }Application submitted
    error{ code?, message }Fatal error (missing/expired token, …)
    resizeinternalIframe height auto-adjusts to content

    Allowed embedding domains#

    The hosted journey sends a Content-Security-Policy: frame-ancestors header listing only registered origins, so browsers refuse to render it inside any unregistered page (clickjacking protection). Until you register your dashboard's origin, the iframe will be blank/blocked.
    Register origins in the marketplace dashboard: Settings → API Credentials → Allowed Embedding Domains, or via the dashboard API:
    GET /api/marketplace-dashboard/embedding-origins
    PUT /api/marketplace-dashboard/embedding-origins
    {
      "live": ["https://app.yourmarketplace.com"],
      "test": ["http://localhost:3000", "https://staging.yourmarketplace.com"]
    }
    Validation rules:
    Origins must be exactly scheme://host[:port] — no path, query, fragment, or credentials.
    live origins must be HTTPS.
    test origins may use HTTP (e.g. http://localhost:3000).

    Step 5 — Funding requests#

    Endpoint: POST /api/v1/marketplace-api/merchants/:ref/funding-requests — or the merchant requests funding inside the iframe; both create the same object.
    {
      "amountRequested": 2000000,
      "tenorDays": 90,
      "purpose": "Inventory purchase",
      "financingTimeframeDays": 7
    }
    FieldRequiredNotes
    amountRequestedyesNGN, min 1,000
    tenorDaysyesOne of 30, 90, 120, 365, 730
    purposeyesInventory purchase, Working capital, Equipment purchase, Business expansion, Marketing & advertising, Other
    financingTimeframeDaysno1–30, default 7

    Response (201)#

    {
      "id": "fr-uuid",
      "status": "PENDING",
      "amountRequested": 2000000,
      "tenorDays": 90,
      "purpose": "Inventory purchase"
    }

    Lifecycle#

    A funding request starts as a pending intent and is fulfilled automatically — you don't call anything else:
    Merchants can request funding while KYC is still SUBMITTED — the intent simply waits for approval.
    A reconciler promotes eligible intents every ~2 minutes. On fulfillment, loanId is set and you receive funding_request.fulfilled; on policy failure you receive funding_request.rejected with failure_reason (e.g. "Requested amount NGN 2,000,000 exceeds lending cap of NGN 1,500,000"). A rejected merchant can simply request a smaller amount (new intent).
    Each call creates a new intent (not idempotent) — deduplicate on id on your side.
    From fulfillment onward, the standard loan lifecycle and its webhooks apply — see the Loan API guide and Webhook guide (loan.approved, loan.disbursed, …).

    Tracking progress#

    Webhooks (recommended)#

    For merchants you onboard through this API, OpenSylo sends these events to your registered webhook URL, signed with the same X-OpenSylo-Signature scheme as all outbound webhooks:
    EventWhen
    merchant.createdMerchant created via POST /merchants
    kyc.submittedKYC submitted (API or iframe)
    kyc.approvedKYC approved by review
    kyc.rejectedKYC rejected — payload includes rejection_reason
    funding_request.createdFunding request created (API or iframe)
    funding_request.fulfilledIntent promoted to a real loan — payload includes loan_id
    funding_request.rejectedPolicy blocked fulfillment — payload includes failure_reason
    Payload examples and the signature-verification recipe are in the Webhook guide.

    Polling#

    Endpoint: GET /api/v1/marketplace-api/merchants/:ref
    {
      "merchantId": "a1b2c3d4-...",
      "thirdPartyCustomerId": "mkt_merchant_8421",
      "connectionStatus": "ACTIVE",
      "kyc": {
        "currentStep": "review",
        "completedSteps": ["business-info", "directors", "documents"],
        "remainingSteps": ["review"],
        "overallProgress": 75,
        "status": "SUBMITTED",
        "canProceed": true,
        "submittedAt": "2026-06-10T10:05:00Z"
      },
      "creditScore": { "score": 75, "computedAt": "2026-06-10T10:00:00Z" },
      "fundingRequests": [
        {
          "id": "fr-uuid",
          "amountRequested": 2000000,
          "tenorDays": 90,
          "purpose": "Inventory purchase",
          "status": "PENDING",
          "loanId": null,
          "failureReason": null
        }
      ]
    }
    KYC status values: PENDING, SUBMITTED, APPROVED, REJECTED, REQUIRES_UPDATE (plus transient internal review states).

    Constraints and gotchas#

    StepVia APIVia iframe
    Create merchant✅❌
    Business info / directors / documents / submit✅✅
    Request funding✅✅
    Sales-data ingestion✅❌
    Mint embed token✅❌
    Accept offers, payout account, debit mandate❌✅
    Secret keys never reach the browser; embed tokens never live longer than 15 minutes. Mint on open, re-mint on error with an expired-token code.
    Sales data is a prerequisite for funding — without a credit score, every funding request will eventually be rejected. Ingest early.
    Async timings: scoring ~5 min after sales-data; intent fulfillment within ~2 min of KYC approval; KYC webhooks may lag admin action by up to ~2 min.
    Popups must be allowed: during payout setup the merchant authorizes a bank-debit mandate that opens in a new tab (the provider cannot run inside an iframe). The SDK's iframe sandbox already permits this; just don't block popups in your own dashboard shell.
    The iframe auto-resizes to its content via the resize bridge event; give the container a sensible width and no fixed height.

    Testing checklist#

    Register http://localhost:<port> under test embedding origins.
    With sk_test_*: create a merchant, confirm the merchant.created webhook and its signature.
    Submit sales data; poll until creditScore populates.
    Mint an embed token, render the journey locally, and complete business info → directors → a document upload → submit.
    Confirm kyc.submitted arrives; after test approval, confirm kyc.approved.
    Create a funding request and observe funding_request.created then funding_request.fulfilled (or rejected with a reason).
    Verify your webhook handler deduplicates on event_id and responds 2xx quickly.

    Go-live checklist#

    Marketplace switched live by OpenSylo; sk_live_* key in your production secrets manager.
    Production dashboard origin (HTTPS) registered under live embedding origins.
    Production webhook URL registered; signature verification confirmed against a live event.
    SDK pinned to a version with integrity, or npm dependency locked.
    environment: "live" passed to ApplicationJourney.
    Modified at 2026-09-02 10:47:25
    Previous
    Webhooks
    Next
    Start OAuth authorization
    Built with