Loading
Loading
Developers · Connect API
Link your app to a creator's show-me-love page, then read their support link and totals with their permission. Plain HTTP, no SDK.
Connect lets your app link one of its users to their show-me-love page. After the user presses Allow, your server can look up their support link (and, if they choose, their totals) with a few plain HTTP calls. There is no SDK to install: everything below uses fetch.
The flow: your server asks for a session, the user approves on show-me-love, we send them back to your callback with a short-lived code, and your server swaps that code for a connection. Each of your users is identified by a user_ref, an id you pick from your own system (1 to 128 characters).
http://localhost:3001/api/sml/callback.SML_ORIGIN=https://your-show-me-love-host
SML_CLIENT_ID=sml_app_xxxxxxxxxxxxxxxx
SML_API_KEY=sml_test_xxxxxxxx # server only, never in the browser
APP_ORIGIN=http://localhost:3001
SML_REDIRECT_URI=http://localhost:3001/api/sml/callback// lib/sml.ts (server only)
export async function sml(path: string, init: { method?: string; body?: unknown } = {}) {
const res = await fetch(process.env.SML_ORIGIN + "/api/connect" + path, {
method: init.method ?? "GET",
headers: {
Authorization: "Bearer " + process.env.SML_API_KEY,
"X-Client-Id": process.env.SML_CLIENT_ID!,
...(init.body ? { "Content-Type": "application/json" } : {}),
},
body: init.body ? JSON.stringify(init.body) : undefined,
cache: "no-store",
});
return { status: res.status, data: await res.json().catch(() => ({})) };
}// app/api/sml/connect/route.ts
import { cookies } from "next/headers";
import { sml } from "@/lib/sml";
export async function POST() {
const userRef = await myOwnSessionUserId(); // from YOUR session, never from the request
const state = crypto.randomUUID();
(await cookies()).set("sml_state", state, { httpOnly: true, sameSite: "lax", maxAge: 600, path: "/" });
const { status, data } = await sml("/sessions", {
method: "POST",
body: { user_ref: userRef, redirect_uri: process.env.SML_REDIRECT_URI, state },
});
if (status !== 200) return Response.json(data, { status: 502 });
return Response.json({ url: data.authorize_url }); // the browser goes there
}// app/api/sml/callback/route.ts
import { cookies } from "next/headers";
import { sml } from "@/lib/sml";
export async function GET(req: Request) {
const q = new URL(req.url).searchParams;
const jar = await cookies();
const saved = jar.get("sml_state")?.value;
jar.delete("sml_state"); // single use, whatever happens next
const home = (qs = "") => Response.redirect(process.env.APP_ORIGIN + "/" + qs);
if (!saved || q.get("state") !== saved) return home("?error=Bad+state");
if (q.get("error") === "access_denied") return home("?denied=1");
const { status, data } = await sml("/token", { method: "POST", body: { code: q.get("code") } }); // 200 { connected: true }
return status === 200 ? home() : home("?error=" + encodeURIComponent(data.error ?? "Could not connect"));
}// Anywhere on the server
const { status, data } = await sml("/connection?user_ref=" + encodeURIComponent(userRef));
if (status === 200) {
// data.support_url, data.display_name, data.username, data.earnings_shared
} else if (status === 404) {
// not connected: show the Connect button
}A complete working app lives in examples/connect-starter in the repository.
Base URL: your show-me-love origin. Every call needs Authorization: Bearer <key> and X-Client-Id: <client id>. POST bodies must be JSON with Content-Type: application/json. Failures return { "error": "…" } with the status shown below. Each app can make 120 calls a minute.
curl -X POST "$SML_ORIGIN/api/connect/sessions" \
-H "Authorization: Bearer $SML_API_KEY" -H "X-Client-Id: $SML_CLIENT_ID" \
-H "Content-Type: application/json" \
-d "{\"user_ref\":\"u_123\",\"redirect_uri\":\"$SML_REDIRECT_URI\"}"/api/connect/sessionsStarts a connection. Returns a one-time link to send the user's browser to.
Body
{ "user_ref": "u_123", "redirect_uri": "https://app.example.com/api/sml/callback", "state": "random-string" }Success
200 { "authorize_url": "https://…/connect/authorize?req=…" } (valid for 10 minutes)Errors
| 401 | Invalid client credentials. | Missing or wrong key, unknown client id, revoked key, or a key from the other mode. |
| 415 | Send JSON. | The Content-Type is not application/json. |
| 400 | Send a JSON object. | The body is not a JSON object. |
| 400 | Send user_ref, redirect_uri and an optional state as strings. | A field is missing or not a string. |
| 400 | user_ref must be 1 to 128 characters. | |
| 400 | state can be at most 256 characters. | |
| 400 | That redirect_uri isn't registered for this app. | It must match one of your redirect links exactly. |
| 429 | Too many requests. | More than 120 calls a minute for this app. |
/api/connect/tokenSwaps the code from your callback for a connection. Call it once per code.
Body
{ "code": "…" }Success
200 { "connected": true }Errors
| 401 | Invalid client credentials. | Missing or wrong key, unknown client id, revoked key, or a key from the other mode. |
| 415 | Send JSON. | The Content-Type is not application/json. |
| 400 | Send a JSON object. | The body is not a JSON object. |
| 400 | Send the code as a string. | |
| 400 | Code is invalid or expired. | Codes last 10 minutes, work once, and only for the app that asked. |
| 429 | Too many requests. | More than 120 calls a minute for this app. |
/api/connect/connection?user_ref=u_123Returns the creator linked to this user, if the link is still active.
Success
200 { "username": "ada", "display_name": "Ada", "support_url": "https://…/@ada", "earnings_shared": false }Errors
| 401 | Invalid client credentials. | Missing or wrong key, unknown client id, revoked key, or a key from the other mode. |
| 400 | Send user_ref. | Missing, or longer than 128 characters. |
| 404 | Not connected. | Never connected, disconnected, or revoked by the creator. |
| 429 | Too many requests. | More than 120 calls a minute for this app. |
/api/connect/earnings?user_ref=u_123Returns total support received, only when the creator ticked the share-earnings option while allowing.
Success
200 { "total_received": "12.50", "currency": "USDT", "treats": 5 } (sandbox apps always get these sample numbers)Errors
| 401 | Invalid client credentials. | Missing or wrong key, unknown client id, revoked key, or a key from the other mode. |
| 400 | Send user_ref. | |
| 403 | Earnings not shared. | The creator did not share earnings with your app. |
| 404 | Not connected. | |
| 429 | Too many requests. | More than 120 calls a minute for this app. |
/api/connect/disconnectEnds the link from your side and cancels any code not yet exchanged. Safe to repeat.
Body
{ "user_ref": "u_123" }Success
200 { "ok": true }Errors
| 401 | Invalid client credentials. | Missing or wrong key, unknown client id, revoked key, or a key from the other mode. |
| 415 | Send JSON. | The Content-Type is not application/json. |
| 400 | Send a JSON object. | The body is not a JSON object. |
| 400 | Send user_ref. | Missing, empty, or longer than 128 characters. |
| 429 | Too many requests. | More than 120 calls a minute for this app. |
The callback: on Allow, the user returns to your redirect link with ?code=…&state=…. On Deny they return with ?error=access_denied&state=…. If you sent no state, none comes back.
SML_API_KEY in browser code, a mobile app, or a public repo. If one leaks, revoke it in Developers and make a new one.user_ref from your own session. Never read it from a form field, query string or request body the browser controls, or anyone could link themselves to someone else's account.state: set it in an httpOnly cookie when you start, and compare it on the callback before exchanging the code.# fragments.access_denied; revoke the app from the Share page and make sure /connection then returns 404 and your UI says not connected.sml_test_…) only work with sandbox apps, and sandbox earnings are sample numbers.sml_live_…) and put it, with the live client id, in your production env. Keep the sandbox values for development.SML_ORIGIN, APP_ORIGIN and SML_REDIRECT_URI to the production addresses./earnings returns the creator's real totals, so only ask for it if you need it.