VIDRIPDevelopers · AccessCreate an application
Vidrip Access

Sign in, entitlements, age, payouts — as a service

Your site keeps its content, its pages and its brand. Vidrip becomes the account, the wallet and the inbox. People sign in to your site with Vidrip; you read what they’ve unlocked and whether they’re verified 18+; creators link a channel and get paid. This reference covers what’s live today: identity, keys and webhooks. Checkout, verification, messaging and statements arrive on top of the same primitives.

1Your sitesends a person to /oauth/authorize
2Vidripsigns them in (or up) and asks for consent
3Your serverswaps the code for tokens; reads claims
4VidripPOSTs a signed webhook when anything changes

Everything is JSON over HTTPS. Tokens are ES256 JWTs. Timestamps are ISO 8601 in UTC. Every response from /oauth/* is Cache-Control: no-store.

Concepts

  • Application. Your integration: a client id, an optional client secret, registered redirect URIs, a webhook endpoint and API keys. Create one at vidrip.app/account → Developers. New applications start in test: everything works, vk_test_ keys only, and any http://localhost redirect is accepted. We switch you live once the services agreement is signed.
  • Pairwise ids. Every person you see has a sub like vu_3f9c…. It’s stable for your application and different for every other application, so nobody can join user tables across partners. You never receive a Vidrip account id, a birth date, an ID document or card data.
  • Two kinds of link. A person signs in to your site with Vidrip (fan scopes). A creator additionally links one of their Vidrip channels to sell on your site (creator:* scopes). Both happen on the same consent screen.
  • Content classes. Your application declares general, mature or adult. It decides which age level a person needs (per region) and which payment rail your tiers use. Set it in your application settings before you request checkout.
  • Test vs live keys. A vk_test_ key works against a test application and against real data it created. A vk_live_ key is only issued once the application is live.

Sign in with Vidrip

Standard OAuth 2.1 authorization code flow with PKCE, OpenID-style tokens. Confidential (server) clients authenticate with a client secret; public (browser or mobile) clients use PKCE and no secret. Any OIDC library that supports discovery will work; the hand-rolled version is four requests.

  1. Send the browser to https://vidrip.app/oauth/authorize with your client_id, a registered redirect_uri, the scopes you need, a random state, and (recommended) a PKCE code_challenge.
  2. Vidrip signs the person in — or creates their account — and shows what you’re asking for. A guest account converts in place, so anything they did before signing up is kept.
  3. The browser comes back to your redirect_uri with ?code=…&state=…. Check the state.
  4. Your server POSTs the code to /oauth/token and receives an access_token, an id_token and a refresh_token.

Authorize

GET/oauth/authorize
FieldTypeNotes
response_typestringrequiredAlways "code".
client_idstringrequiredYour application’s client id (va_…).
redirect_uriurlrequiredExactly one of your registered URIs. Test apps also accept any http://localhost URL.
scopestringoptionalSpace-separated. Defaults to profile. openid is accepted and ignored.
statestringoptionalOpaque value echoed back. Use it to stop CSRF.
code_challengestringoptionalPKCE, base64url(SHA-256(code_verifier)). Required for public clients.
code_challenge_methodstringoptionalOnly S256.
noncestringoptionalEchoed in the id_token as nonce.
GET https://vidrip.app/oauth/authorize
  ?response_type=code
  &client_id=va_9Kq2…
  &redirect_uri=https%3A%2F%2Fvelour.club%2Fauth%2Fvidrip%2Fcallback
  &scope=profile%20age%20entitlements
  &state=8f2d1c
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256

Unknown client or unregistered redirect → an error page on vidrip.app, never a redirect. Other errors bounce back to you as ?error=…&error_description=…&state=… (invalid_scope, unsupported_response_type, access_denied when the person cancels).

Token

POST/oauth/token

Form-encoded or JSON. Authenticate with HTTP Basic (client_id:client_secret) or with client_id and client_secret in the body. Public clients send client_id and their code_verifier.

FieldTypeNotes
grant_typestringrequiredauthorization_code or refresh_token.
codestringrequired*The code from the redirect (authorization_code).
redirect_uriurloptionalMust equal the one used at authorize, if sent.
code_verifierstringrequired*When a code_challenge was sent. 43–128 chars.
refresh_tokenstringrequired*For grant_type=refresh_token.
scopestringoptionalOn refresh: a subset of the granted scopes, never more.
POST https://vidrip.app/oauth/token
Authorization: Basic base64(va_9Kq2…:vs_…)
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=vc_…&redirect_uri=https://velour.club/auth/vidrip/callback&code_verifier=…
{
  "access_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjNmOWM…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "vr_…",
  "scope": "profile age entitlements",
  "id_token": "eyJhbGciOiJFUzI1NiIs…"
}

Codes are single-use and live five minutes. Access tokens live one hour. Refresh tokens live 30 days and rotate on every use; presenting a rotated one revokes every token for that person’s link, so store the newest.

The ID token

A signed snapshot of the person at sign-in. Verify it with the key at /oauth/jwks (ES256), check iss is https://vidrip.app and aud is your client id. Claims depend on the scopes granted.

{
  "iss": "https://vidrip.app",
  "aud": "va_9Kq2…",
  "sub": "vu_3f9c4a1e7b2d8c6f0a9e5d4c3b2a1f0e",
  "iat": 1790000000, "exp": 1790003600, "auth_time": 1790000000, "nonce": "…",
  "scope": "profile email age entitlements",
  "handle": "samwise",
  "name": "Sam",                      // the name they chose to show YOUR site
  "vidrip_name": "Sam Gamgee",        // their Vidrip display name
  "picture": "https://…/avatar.jpg",
  "email": "[email protected]",         // only with the email scope AND the person's tick on consent
  "email_verified": true,
  "age": { "verified": false, "level": 0, "method": null, "at": null, "expires": null },
  "entitlements": [],
  "channel": { "id": "…", "name": "Sam's Room", "slug": "sam", "picture": "…" }   // creator scopes only
}
ClaimTypeNotes
substringPairwise id for your application. Stable forever. Key your user table on this.
namestringWhat to display. The person can choose a name for your site that differs from their Vidrip name.
emailstringPresent only if you asked for the email scope and the person agreed on the consent screen. Never assume it.
ageobjectlevel 0 none · 1 attested 18+ · 2 verified with ID. Levels arrive with the verification release; the shape is final today.
entitlementsarraySubscriptions the person holds to channels your creators linked, from any rail or source. See Entitlements.
channelobjectThe Vidrip channel a creator linked. Present when creator:* scopes were granted.

Userinfo

GET/oauth/userinfo

Bearer the access token; get the same claims as the id token, fresh from the database. This is how you re-read the age claim or entitlements later without a new sign-in.

GET https://vidrip.app/oauth/userinfo
Authorization: Bearer eyJhbGciOiJFUzI1NiIs…

Refresh & revoke

POST/oauth/token (grant_type=refresh_token)
POST/oauth/revoke

Revoke takes token=vr_… with client authentication. Add token_type_hint=link to end every session for that person, so their next visit asks for consent again. Always answers 200 {}. Access tokens cannot be recalled early; they expire within the hour.

Discovery

DocumentURL
openid-configurationhttps://vidrip.app/.well-known/openid-configuration
jwkshttps://vidrip.app/oauth/jwks

Subject type is pairwise, signing is ES256, PKCE method is S256, token endpoint auth is client_secret_basic, client_secret_post or none. Point your OIDC library at the issuer and it will find the rest.

Scopes

ScopeWhoWhat you get
profilepersonhandle, name, picture. The default.
emailpersonTheir email — only if they tick the box on consent.
agepersonThe age claim: verified, level, method, dates. Never a birth date.
entitlementspersonTheir subscriptions and purchases with your application.
messagespersonRead and send in conversations your application hosts.
creator:tierscreatorCreate and edit tiers on the linked channel.
creator:subscriberscreatorWho subscribes to the linked channel.
creator:messagescreatorThe creator’s conversations with their subscribers on your application.
creator:earningscreatorStatements and payouts for the linked channel.

Requesting any creator:* scope turns the consent screen into a channel picker: the creator chooses which of their Vidrip channels to link. A creator with no channel is sent to make one first.

API keys

Server-to-server calls under /api/access/* carry an application key. Create them in your application; keep them on your server.

Authorization: Bearer vk_test_…      # or vk_live_… once your application is live
# or
x-api-key: vk_test_…
GET/api/access/ping
{ "ok": true, "mode": "test", "app": { "id": "…", "name": "Velour", "clientId": "va_…", "status": "test", "merchantMode": "partner", "contentClasses": ["adult"] } }

People

Everyone who has signed in to your application with Vidrip. The shape matches the id token, so one type covers both.

GET/api/access/fans?limit=50&next=…
{
  "fans": [{
    "sub": "vu_3f9c…", "handle": "samwise", "name": "Sam", "picture": "…",
    "scopes": ["profile", "age", "entitlements"], "linked_at": "2026-09-26T18:02:11Z", "last_seen_at": "…",
    "age": { "verified": false, "level": 0, "method": null, "at": null, "expires": null },
    "entitlements": []
  }],
  "next": "2026-09-26T18:02:11Z"      // pass back as ?next= for the next page; null at the end
}
GET/api/access/fans/:sub
PATCH/api/access/fans/:sub { display_name }
DELETE/api/access/fans/:sub

DELETE disconnects the person from your application: their link and every refresh token are revoked and a link.revoked webhook fires. They can connect again any time.

Creators

GET/api/access/creators
{
  "creators": [{
    "id": "…", "sub": "vu_8a1d…", "handle": "chase", "name": "Chase", "picture": "…",
    "channel": { "id": "…", "name": "Chase's Room", "slug": "chase", "picture": "…" },
    "scopes": ["creator:tiers", "creator:subscribers"], "payouts_ready": true, "linked_at": "…"
  }]
}

payouts_ready is the creator’s Stripe Connect state, which general and mature tiers need before a live checkout. Use id for the tier endpoints below.

Tiers

Subscription tiers live on the creator’s Vidrip channel, so a tier you create is also sellable on Vidrip. With the creator:tiers scope you can create and edit them. Your application can hold up to five per channel, priced at any whole-cent amount from $2.99 to $499.99 a month (price_cents 299–49999). Tiers you create are sold only on your site and never appear in the Vidrip app; the creator’s own Vidrip tiers stay on the App Store price ladder.

GET/api/access/creators/:id/tiers
POST/api/access/creators/:id/tiers
PATCH/api/access/creators/:id/tiers/:tierId
FieldTypeNotes
titlestringrequiredUp to 60 characters. Moderated.
price_centsintegerrequiredOne of 499 599 699 799 899 999 1099 1199 1299 1399 1499 1599 1699 1799 1899 1999 2499 4999 7499 9999.
descriptionstringoptionalUp to 120 characters. Moderated.
content_classstringoptionalgeneral · mature · adult. Must be enabled on your application; defaults to your first class. Decides the age level and the payment rail.
activebooleanoptionalInactive tiers can’t start a checkout. Existing subscribers keep access.
{ "tier": { "id": "…", "channel_id": "…", "idx": 0, "title": "Backstage", "price_cents": 999, "description": "Everything, a day early.",
            "active": true, "content_class": "adult", "created_by_app": true } }

A price change applies to new subscribers; people already subscribed keep the amount they signed up at. Every create or edit fires tier.updated.

Checkout

Create a session on your server and send the person to its url. Access signs them in or up, connects them to your application (profile, entitlements, age), takes the 18+ confirmation for mature and adult tiers, and charges on the rail the tier’s class needs. They come back to your success_url with ?session_id=….

POST/api/access/checkout/sessions
FieldTypeNotes
tier_idstringrequiredA tier on a channel the creator linked to your application.
success_urlurlrequiredAbsolute https URL. We append session_id.
cancel_urlurloptionalWhere “Cancel” goes.
client_referencestringoptionalYour own id for this attempt, echoed back.
metadataobjectoptionalAnything you want to read back later.
{ "id": "6a2f…", "url": "https://vidripaccess.com/pay/6a2f…", "rail": "stripe", "status": "open", "expires_at": "2026-09-27T18:00:00Z" }
GET/api/access/checkout/sessions/:id
{
  "id": "6a2f…", "status": "paid", "rail": "stripe", "sub": "vu_3f9c…", "client_reference": "order_81",
  "channel": { "id": "…", "name": "Chase's Room", "slug": "chase" }, "tier": { "id": "…", "title": "Backstage", "price_cents": 999, "content_class": "general" },
  "completed_at": "2026-09-26T19:04:12Z",
  "entitlement": { "id": "…", "kind": "subscription", "status": "active", "current_period_end": "2026-10-26T19:04:12Z", "cancel_at_period_end": false, "source": "partner", "provider": "stripe", "…": "…" }
}

Rails

  • general · mature → Stripe. The card form is inline on the pay page. The creator is paid through Vidrip’s Stripe Connect platform; Access keeps 5% and your configured share is set aside for you.
  • adult → your processor. You are the merchant of record. The pay page hands off to your processor’s hosted form and we take the postback. See Adult processors.
  • test. A test application without a processor gets a simulator button on adult tiers: no card, a 30-day subscription, real webhooks. Build the whole flow before underwriting clears.

Sessions expire after 24 hours. Rely on the entitlement.granted webhook to unlock; use the GET for your success page. Someone who already subscribes to that channel is sent straight back with their existing entitlement.

Billing portal

Link people to https://vidripaccess.com/pay/manage to see, cancel and resume every subscription. No parameters; they sign in with the same Vidrip account.

Entitlements

An entitlement is a subscription a person holds to a channel one of your creators linked, wherever it was bought: through your checkout, on vidrip.app, or inside the Vidrip app. Read them on the id token, from /oauth/userinfo, on GET /api/access/fans/:sub, or on the checkout session.

{
  "id": "…", "sub": "vu_3f9c…", "kind": "subscription",
  "status": "active",                       // active · past_due · canceled · incomplete
  "channel": { "id": "…", "name": "Chase's Room", "slug": "chase" },
  "tier": { "id": "…", "title": "Backstage", "price_cents": 999, "content_class": "general" },
  "current_period_end": "2026-10-26T19:04:12Z", "cancel_at_period_end": false,
  "source": "partner",                      // partner (your checkout) · vidrip (web) · iap (App Store / Play)
  "provider": "stripe",                     // stripe · appstore · googleplay · ccbill · segpay · test
  "started_at": "…", "updated_at": "…"
}

Gate content on status === "active". Treat past_due as still active with a nudge to fix the card; canceled means access has ended (period over, refunded or disputed). cancel_at_period_end on an active entitlement is a chance to win them back before current_period_end.

Adult processors

Adult content can’t be charged through Stripe. In partner mode you hold the merchant account and Access drives it: enter the account’s credentials under Payments in your application, give the processor the postback URL the dashboard shows you, and adult tiers switch from the simulator to the real hosted form. Cards never touch Access or your servers; only the processor sees them.

CCBill fieldWhere it comes from
clientAccnumrequiredYour CCBill client account number.
clientSubaccrequiredThe subaccount for this site.
flexFormIdrequiredA FlexForm configured for dynamic pricing.
saltrequiredThe encryption salt CCBill support enables for dynamic pricing.
postbackSecretoptionalAny random string. It goes into the postback URL and we check it on every event.
datalinkUsername / datalinkPasswordoptionalDataLink credentials, so people can cancel from the billing portal. Without them, cancellations happen on the processor’s side.

The CCBill adapter follows FlexForms dynamic pricing and Webhooks 2.0. It has not yet been exercised against a live merchant account (26 Sep 2026), and the Segpay adapter is pending its account, so expect both to be tightened when the first partner’s underwriting clears.

Age verification

Verify once, honour everywhere. A person proves they are 18+ a single time with Vidrip, and every application they connect to reads the same claim. Vidrip stores only the result. Documents and selfies stay with the verification provider; no birth date, name or image ever reaches Vidrip or you.

LevelMeaning
0Nothing yet.
1Attested: ticked "I am 18 or older" at a checkout for a mature or adult tier.
2Verified: a government ID with a selfie match. Valid 12 months. (A cheaper selfie-only age estimate is planned; v1 is document only.)

Who needs which level

Policy is data, not code: a table of region × content class → required level, evaluated from the person’s own location at checkout. Adult content needs level 2 in the UK and in the US states with age-verification statutes, and level 1 elsewhere; mature content needs level 1 everywhere. Ask what applies:

GET/api/access/policies?region=US-TX&content_class=adult
{ "region": "US-TX", "content_class": "adult", "required_level": 2, "estimation_ok": true, "matched": "US-TX" }

How someone gets verified

  • Automatically at checkout. When a person’s region needs level 2 for the tier they’re buying, the pay page sends them through verification and back. You do nothing.
  • On demand. Start a session for someone who already signed in with Vidrip and send them to its url. Useful for gating free adult content, or for verifying a creator before they sell.
  • From their Vidrip account. A person or creator can verify themselves without any partner involved.
POST/api/access/verification/sessions
FieldTypeNotes
substringrequiredThe person’s pairwise id.
return_urlurlrequiredWhere they land afterwards; we append verification=passed|failed|review.
levelintegeroptional1 or 2. Defaults to 2.
content_classstringoptionalWhich class this is for; defaults to adult if your app has it.
estimation_okbooleanoptionalReserved for the selfie age estimate; ignored in v1 (document only).
{ "id": "9b1e…", "url": "https://vidripaccess.com/verify/9b1e…", "status": "open", "required_level": 2, "current_level": 1, "already_satisfied": false, "…": "…" }

// already verified → no session is created:
{ "id": null, "url": null, "required_level": 2, "current_level": 2, "already_satisfied": true }
GET/api/access/verification/sessions/:id
GET/api/access/verification/:sub
{ "sub": "vu_3f9c…",
  "age": { "verified": true, "level": 2, "method": "document", "at": "2026-09-26T20:11:04Z", "expires": "2027-09-26T20:11:04Z" },
  "sessions": [ { "id": "9b1e…", "purpose": "age", "status": "passed", "required_level": 2, "method": "document", "failure_reason": null, "created_at": "…", "completed_at": "…" } ] }

The same age object appears on the id token, /oauth/userinfo and the people endpoints. method is attestation or document (age_estimation once the selfie option ships). Failures carry a reason: underage, estimation_inconclusive, declined. Webhooks: verification.passed, verification.failed, verification.expired.

Performers

Card-brand rules for adult platforms require everyone who appears in content to be verified, and the creator to be verified before selling. Access handles both: a creator can’t create an adult tier until they hold level 2 (403 creator_not_verified), and you can invite performers to verify through a link. The performer signs in or up with Vidrip, verifies with an ID, and you get an id to attach to the post. You never receive their name or documents.

POST/api/access/performers { creator_link_id, name? }
GET/api/access/performers?creator_link_id=…
GET/api/access/performers/:id
DELETE/api/access/performers/:id
{ "id": "c4d7…", "creator_link_id": "…", "name": "Sam (stage name)", "status": "invited", "verified": false, "url": "https://vidripaccess.com/verify/1e0a…", "created_at": "…" }

Send url to the performer. When they finish you get performer.verified (or performer.failed) and status flips. Store the performer id on every post they appear in; that record is what a processor audit asks for.

Messaging

Conversations between a person and a creator, hosted by your application. The creator reads and replies from the Vidrip app, with a push for every new message. You either drop the hosted inbox into your site or build your own on the API. Vidrip’s own DMs are a separate thing; you only ever see conversations your application hosts.

The hosted inbox

Mint a token for a signed-in person on your server, then put the URL in an iframe. The token lives an hour; mint one per page load.

POST/api/access/embed/tokens { sub, thread_id? }
{ "token": "eyJhbGciOiJFUzI1NiIs…", "expires_in": 3600, "sub": "vu_3f9c…", "url": "https://vidripaccess.com/embed/inbox?token=…" }
<iframe src="https://vidripaccess.com/embed/inbox?token=…&creator=<creator_link_id>" allow="camera; microphone" style="width:100%;height:640px;border:0"></iframe>

Add &creator=<creator_link_id> to open (or start) the conversation with one creator, or &thread=<id> for a specific thread. Colours and font come from your application’s embed_theme (PATCH /api/access/apps/:id with { bg, surface, text, muted, accent, accentFrom, accentTo, font, radius }). The allow attribute is what lets people record a video reply inside the frame.

Your own UI

Use the same token as a Bearer against the inbox endpoints. Everything the hosted inbox does, it does through these.

GET/api/access/inbox/me
GET/api/access/inbox/threads
POST/api/access/inbox/threads { creator_link_id } | { sub }
GET/api/access/inbox/threads/:id?after=<iso>
POST/api/access/inbox/threads/:id/messages { kind, body?, media_key?, poster_key?, duration_ms? }
POST/api/access/inbox/threads/:id/read
POST/api/access/inbox/threads/:id/block { blocked }
POST/api/access/inbox/threads/:id/report { reason, detail?, message_id? }
POST/api/access/inbox/upload (multipart: file, kind=video|image|poster)
POST/api/access/inbox/broadcasts { kind, body, … } (creators)
{ "id": "…", "thread_id": "…", "author_role": "fan", "kind": "video", "body": null,
  "media_url": "https://vidripaccess.com/api/media/clip?…",   // signed, valid an hour; null when media_hidden
  "poster_url": "…", "duration_ms": 8400, "media_hidden": false, "removed": false, "broadcast": false, "created_at": "…" }

Poll ?after= with the newest created_at you have; there is no socket. Text is moderated before it lands. Media must be uploaded through /inbox/upload by the same person first. Sixty messages an hour per person; three broadcasts a day per creator.

Server side

GET/api/access/threads?sub=&creator_link_id=&limit=&next=
GET/api/access/threads/:id/messages?limit=&before=
POST/api/access/broadcasts { creator_link_id, tier_id?, body } (needs creator:messages)
GET/api/access/reports?status=open
PATCH/api/access/reports/:id { status, resolution?, remove_message? }

Reports are yours to resolve; the card brands expect it within seven days for adult platforms. remove_message hides the reported message for everyone. Vidrip ops can see every report across applications.

In the Vidrip app

The creator gets a push for each new message and opens the conversation inside the app. For adult-class applications the push carries no preview and the app never renders inbound media; the creator sees “available on the web” and answers with text or a camera clip. Nothing explicit is ever drawn by the app, which is what keeps its store listing safe.

Payouts & ledger

Two rails, one ledger. Every charge, refund and dispute on your application writes a row split into processor fee, Access’s 5%, your share and the creator’s net. Statements are a sum over those rows; payout runs pay creators from them; the 1099 export is what a payer actually paid.

  • Stripe rail (general and mature). Vidrip is the Connect platform. The creator’s net leaves by destination charge as each invoice is paid. Your share accrues in the ledger and Vidrip transfers it to your own Connect account each month from the closed statement. Set that account up under Payouts in your application.
  • Adult rail (your processor). You are merchant of record and receive gross. You pay creators from payout runs; you owe Access 5% of gross, invoiced monthly from the statement. You are the payer of record for 1099s, and creators give you their W-9 through Vidrip.
GET/api/access/ledger?from=&to=&kind=&rail=&limit=&next=[&format=csv]
{ "entries": [{ "id": "…", "rail": "ccbill", "kind": "charge", "currency": "usd", "gross_cents": 999, "processor_fee_cents": 125, "access_fee_cents": 50, "partner_fee_cents": 150,
  "creator_net_cents": 674, "processor_ref": "1234567890", "checkout_id": "…", "paid_in_run": null, "occurred_at": "…" }], "next": "…" }
GET/api/access/statements?period=YYYY-MM[&format=csv]
{ "statement": { "period": "2026-10", "gross_cents": 48950, "refunds_cents": -999, "disputes_cents": 0, "processor_fee_cents": 6119, "access_fee_cents": 2447,
    "partner_fee_cents": 7343, "creator_net_cents": 33041, "access_fee_due_cents": 2447, "partner_share_due_cents": 0,
    "by_rail": { "ccbill": { "gross_cents": 48950, "net_cents": 33041, "entries": 51 } },
    "by_creator": [{ "sub": "vu_8a1d…", "name": "Chase", "gross_cents": 29970, "refunds_cents": 0, "creator_net_cents": 20229, "paid_cents": 13486, "pending_cents": 6743 }] },
  "closed": { "id": "…", "status": "final", "partner_transfer_ref": null } }

Payout runs (adult rail)

A run gathers every creator’s unpaid balance up to a date, skipping anyone under $20 (carried forward), without a payout account, or crossing the $600 threshold without a tax form. A signed-in reviewer approves it; the API cannot. Then you download the Paxum Mass Pay or bank CSV, upload it, and mark the run completed so creators are told and those rows close.

GET/api/access/payouts/runs
POST/api/access/payouts/runs { period_end? }
GET/api/access/payouts/runs/:id[?format=csv]
POST/api/access/payouts/runs/:id/approve (session only)
POST/api/access/payouts/runs/:id/complete { batch_ref?, items?: [{ id, status: 'sent'|'failed', provider_ref? }] }
POST/api/access/payouts/runs/:id/cancel
GET/api/access/payouts/tax-export?year=

Failed items release their balance for the next run. The tax export decrypts W-9 data and is the only place it ever leaves the database; it lists W-8BEN holders separately. Creators enter payout details and tax forms in their Vidrip account under Partner payouts.

Guard rails

A creator whose disputes exceed 1% of charges over 90 days, with at least three disputes, has their tiers on your application paused automatically and you receive creator.paused. Processor fees on the adult rail are estimated from the rate you enter under Payouts; reconcile against your processor’s statement.

Webhooks

Set an endpoint URL and generate a signing secret in your application. We POST JSON with three headers and retry on anything but a 2xx: after 1 minute, 5, 30, 2 hours, 6, 24, then mark the event failed. You can replay any event from the dashboard.

POST https://velour.club/webhooks/vidrip
Content-Type: application/json
Vidrip-Event-Id: 6c1e…
Vidrip-Event-Type: link.created
Vidrip-Signature: t=1790000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

{ "id": "6c1e…", "type": "link.created", "created_at": "…", "app_id": "…", "client_id": "va_…",
  "data": { "sub": "vu_3f9c…", "scopes": ["profile", "age"], "kind": "fan" } }

Verify the signature

import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyVidripSignature(secret, header, rawBody, toleranceSec = 300) {
  const m = /t=(\d+),v1=([0-9a-f]+)/.exec(header ?? '');
  if (!m) return false;
  const ts = Number(m[1]);
  if (Math.abs(Date.now() / 1000 - ts) > toleranceSec) return false;          // replay window
  const expected = createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
  return expected.length === m[2].length && timingSafeEqual(Buffer.from(expected), Buffer.from(m[2]));
}

Sign over the raw request body, not a re-serialised object. Answer 2xx quickly and do the work afterwards; we time out after 10 seconds.

Event types

TypeWhendata
pingYou press “Send test event”{ message, sent_at }
link.createdA person connects (or reconnects) to your application{ sub, scopes, kind: "fan" }
link.revokedYou or the person disconnect{ sub, by: "partner" | "user" }
creator_link.createdA creator links a channel{ sub, channel: { id, name, slug }, scopes }
entitlement.grantedA person’s subscription to a linked channel becomes active — any rail, any source{ entitlement, sub, previous_status }
entitlement.renewedA renewal extended current_period_end{ entitlement, sub, previous_status }
entitlement.past_dueA renewal failed and the processor is retrying{ entitlement, sub, previous_status }
entitlement.revokedAccess ended: period over after cancel, refund, dispute, or retries exhausted{ entitlement, sub, previous_status }
tier.updatedA tier was created or changed through the API{ tier, change: "created" | "updated", fields? }
verification.passedA person you’re linked to reached level 2{ sub, age, session_id, purpose }
verification.failedA check ended without a pass{ sub, age, reason, session_id }
verification.expiredA check expired before finishing{ sub, age, session_id }
performer.verifiedA performer invite completed{ performer: { id, creator_link_id, performer_name } }
performer.failedA performer invite failed{ performer, reason }
message.createdA new message in a conversation you host (either side){ thread_id, sub, message: { …, has_media } }
broadcast.sentA creator’s broadcast finished fanning out{ broadcast_id, creator_link_id, tier_id, recipient_count, kind }
report.createdSomeone reported a conversation or message{ report_id, thread_id, message_id, sub, by, reason }
thread.blocked / thread.unblockedA participant blocked or unblocked the other{ thread_id, sub, by }
ledger.entryA charge, refund, dispute or reversal was recorded{ entry }
statement.readyLast month’s statement closed (first pump after month end){ period, gross_cents, …, access_fee_due_cents, partner_share_due_cents, creators }
payout.run.created / .approvedA payout run was drafted or approved{ run_id, total_cents, item_count }
payout.completedA payout run was marked paid{ run_id, total_cents, item_count, provider, batch_ref }
partner_share.paidVidrip transferred your Stripe-rail share{ period, amount_cents, transfer }
creator.pausedA creator’s tiers were paused for dispute rate{ channel_ids, disputes_90d, charges_90d, rate, reason }

Errors

OAuth endpoints answer RFC 6749 style: { "error": "invalid_grant", "error_description": "code expired" }. API endpoints answer { "error": "…" } with the status you’d expect.

StatusMeaning
400Bad input; the message says which field.
401Missing or invalid key, secret, code or token.
403Live key on a test application, suspended application, or a channel that isn’t yours.
404Unknown application, person or request.
409The consent request was already answered.
410The consent request expired (10 minutes).
503Access isn’t configured on this deployment (signing key missing).

Examples

Node · Express

import crypto from 'node:crypto';
const ISSUER = 'https://vidrip.app', CLIENT_ID = process.env.VIDRIP_CLIENT_ID, SECRET = process.env.VIDRIP_CLIENT_SECRET;
const REDIRECT = 'https://velour.club/auth/vidrip/callback';

app.get('/auth/vidrip', (req, res) => {
  const state = crypto.randomBytes(16).toString('hex');
  const verifier = crypto.randomBytes(32).toString('base64url');
  const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
  req.session.vidrip = { state, verifier };
  const u = new URL(ISSUER + '/oauth/authorize');
  u.search = new URLSearchParams({ response_type: 'code', client_id: CLIENT_ID, redirect_uri: REDIRECT,
    scope: 'profile age entitlements', state, code_challenge: challenge, code_challenge_method: 'S256' }).toString();
  res.redirect(u.toString());
});

app.get('/auth/vidrip/callback', async (req, res) => {
  const { state, verifier } = req.session.vidrip ?? {};
  if (!state || req.query.state !== state) return res.status(400).send('bad state');
  const r = await fetch(ISSUER + '/oauth/token', {
    method: 'POST',
    headers: { 'content-type': 'application/x-www-form-urlencoded',
               authorization: 'Basic ' + Buffer.from(`${CLIENT_ID}:${SECRET}`).toString('base64') },
    body: new URLSearchParams({ grant_type: 'authorization_code', code: String(req.query.code), redirect_uri: REDIRECT, code_verifier: verifier }),
  });
  const tokens = await r.json();
  const claims = JSON.parse(Buffer.from(tokens.id_token.split('.')[1], 'base64url'));   // verify the signature with /oauth/jwks in production
  await db.users.upsert({ vidrip_sub: claims.sub, name: claims.name, picture: claims.picture, age_level: claims.age?.level ?? 0 });
  req.session.user = claims.sub; req.session.vidrip_refresh = tokens.refresh_token;
  res.redirect('/');
});

curl

curl -s https://vidrip.app/api/access/ping -H "Authorization: Bearer $VIDRIP_KEY"
curl -s "https://vidrip.app/api/access/fans?limit=20" -H "Authorization: Bearer $VIDRIP_KEY"
curl -s https://vidrip.app/api/access/creators -H "Authorization: Bearer $VIDRIP_KEY"
curl -s -X POST https://vidrip.app/api/access/webhooks/pump -H "Authorization: Bearer $VIDRIP_KEY"   # deliver anything due for your app now

What’s next

  • One-off purchases and annual tiers. The entitlement object already carries a kind; pay-per-view and yearly pricing arrive after the first partner’s beta.
  • Managed mode. Vidrip Access holding the adult processor and payout accounts itself, for partners who want nothing to sign. Formed when the first such partner arrives.
Ready to add Sign in with Vidrip?

Create an application, register a redirect, run the flow in test.