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:| Surface | Auth | Used by | Purpose |
|---|
Marketplace API (/api/v1/marketplace-api/*) | Secret key (sk_test_* / sk_live_*) | Your backend | Create merchants, prefill KYC, ingest sales data, mint embed tokens, create funding requests, poll status |
| Embedded journey (iframe via Embed SDK) | Short-lived embed token | The merchant, in your dashboard | Business 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#
| Requirement | Where |
|---|
| Marketplace account and dashboard access | OpenSylo admin invitation |
Secret keys (sk_test_*, sk_live_*) | Marketplace dashboard → Settings → API Credentials |
Webhook endpoint registered + apiSecret for signature verification | Marketplace dashboard → Settings → API Credentials |
| Allowed embedding domains registered | Marketplace 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
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/merchantsCreates (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.| Field | Required | Notes |
|---|
thirdPartyCustomerId | yes | Your stable ID for the merchant (max 255). Idempotency key, echoed in every webhook. |
businessName | yes | Max 150 |
email | yes | Valid email |
phoneNumber | yes | E.164, e.g. +2348012345678 |
firstName, lastName | no | Primary contact (max 50 each) |
businessType | no | e.g. LIMITED_LIABILITY_COMPANY, SOLE_PROPRIETORSHIP |
industry | no | e.g. FOOD |
businessCategory | no | Free text, max 50 |
externalReference | no | Stored 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.Step 2 — Submit sales data#
Endpoint: POST /api/v1/marketplace-api/merchants/:ref/sales-dataCredit 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"
}
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.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/directorsTakes a directors array. Each director:| Field | Required | Validation |
|---|
firstName, lastName | yes | 2–50 chars, letters/spaces/hyphens/apostrophes |
middleName | no | Max 50 |
email | yes | Valid email |
phoneNumber | yes | E.164 |
address | yes | Max 500 |
dateOfBirth | yes | ISO 8601, in the past |
nationality | yes | Max 50 |
identityType | yes | BVN, NIN, PASSPORT, or DRIVERS_LICENSE |
identityNumber | yes | BVN/NIN: exactly 11 digits. Passport/licence: 5–20 alphanumeric |
position | no | CEO, CTO, CFO, COO, Director, Chairman, Secretary, Shareholder |
isPrimary | no | Marks 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)#
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#
| Option | Type | Required | Description |
|---|
token | string | yes | Embed token from your backend |
container | string | HTMLElement | yes | Where to mount the iframe |
environment | "test" | "live" | no | Defaults to "test" |
host | string | no | Origin of the hosted journey. Defaults to https://embed.opensylo.com |
theme.primary | string | no | Brand color, e.g. #ff5500 |
theme.logo | string | no | Logo URL shown at the top of the journey |
theme.radius | string | no | Corner radius, e.g. 12px |
onReady | function | no | Journey mounted with a valid token |
onStepChange | function | no | { step } — merchant advanced a step |
onCompleted | function | no | { merchantId? } — application submitted |
onError | function | no | { 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.| Event | Payload | When |
|---|
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, …) |
resize | internal | Iframe 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"]
}
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
}
| Field | Required | Notes |
|---|
amountRequested | yes | NGN, min 1,000 |
tenorDays | yes | One of 30, 90, 120, 365, 730 |
purpose | yes | Inventory purchase, Working capital, Equipment purchase, Business expansion, Marketing & advertising, Other |
financingTimeframeDays | no | 1–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:| Event | When |
|---|
merchant.created | Merchant created via POST /merchants |
kyc.submitted | KYC submitted (API or iframe) |
kyc.approved | KYC approved by review |
kyc.rejected | KYC rejected — payload includes rejection_reason |
funding_request.created | Funding request created (API or iframe) |
funding_request.fulfilled | Intent promoted to a real loan — payload includes loan_id |
funding_request.rejected | Policy 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#
| Step | Via API | Via 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#
Go-live checklist#
Modified at 2026-09-02 10:47:25