API reference

Invitations

  • 3 endpoints

Create invitation#

POST /invitations

Invites someone: to the team (role admin, from the dashboard only, both modes) or as a member in the request's mode (role user: a test key invites a test member). The link is returned, and emailed to the person unless sendEmail is false. A new invitation replaces the open one of the same person and mode. Someone already there with that role and mode → 409 already_joined. An organization sends at most 50 invitations an hour, both modes together (429 rate_limit_exceeded). Answers the invitation and its link (only in this response).

Auth
X-API-Key: an admin key
On-Behalf-Of
Refused: the admin role is needed
Idempotency-Key
Accepted: send one per operation
Success
201
Request

request.sh

curl https://api.qbitflow.app/v2/invitations \  -H "X-API-Key: $QBITFLOW_API_KEY" \  -H "Content-Type: application/json" \  -H "Idempotency-Key: $(uuidgen)" \  -d '{    "email": "seller@example.com",    "role": "user",    "holdFunds": true,    "organizationFeePercent": "10",    "redirectUrl": "https://shop.example.com/sellers/welcome"  }'# 201: a CreatedInvitation
const created = await client.invitations.create({	email: 'seller@example.com',	holdFunds: true, // hold their payments until you trust them (always sent)	organizationFeePercent: '10', // your commission on their payments: '0' to '50' %	redirectUrl: 'https://shop.example.com/sellers/welcome',});// QBitFlow also emails them a link of their own. They are a member once they accepted (the// member.joined webhook).console.log(`invitation ${created.uuid}: ${created.link}`);
created = client.invitations.create(    email="seller@example.com",    hold_funds=True,  # hold their payments until you trust them (always sent)    organization_fee_percent="10",  # your commission on their payments: "0" to "50" %    redirect_url="https://shop.example.com/sellers/welcome",)# The seller exists once they accept: wait for the member.joined webhook.# created.uuid is what member.joined's invitation_uuid names; its user_uuid is the seller.# QBitFlow also emails them a link of their own.print("invitation", created.uuid, "link:", created.link)
created, err := client.Invitations.Create(ctx, &qbitflow.CreateInvitationParams{	Email:                  "seller@example.com",	HoldFunds:              true, // hold their payments until Members.Trust (always sent)	OrganizationFeePercent: "10", // your commission on their payments: "0" to "50" %	RedirectURL:            "https://shop.example.com/sellers/welcome",})if err != nil {	log.Fatal(err) // 409 already_joined for a member}// QBitFlow also emails the seller a link of their own. The seller exists once they// accepted: wait for member.joined.fmt.Println("invitation", created.UUID, "link:", created.Link)
$created = $client->invitations->create(new \QBitFlow\Params\CreateInvitationParams(	email: 'seller@example.com',	holdFunds: true,              // hold their payments until you trust them (always sent)	organizationFeePercent: '10', // your commission on their payments: "0" to "50" %	redirectUrl: 'https://shop.example.com/sellers/welcome',));// QBitFlow also emails them a link of their own.echo "Invitation {$created->uuid}: {$created->link}\n";

Body

  • emailstringRequired

    emailmax=254

  • rolestringRequired

    admin (the team, from the dashboard) or user (a member, in the request's mode)

    oneof=admin user

  • holdFundsbooleanOptional

    Members, required: true = the organization holds their payments until it trusts them (POST /members/:userUuid/trust); false = paid directly. Not for the team

  • organizationFeePercentnumberOptional

    Members: the organization's fee on their payments, 0 to 50 % (default 0). Not for the team

    percent=50

  • redirectUrlstringOptional

    Optional: where the person goes once they accepted (with ?invitationUuid=<uuid>). A convenience: confirm with GET /invitations or the member.joined event, not from the redirect

    http_urlmax=2048

  • sendEmailbooleanOptional

    Optional, default true: QBitFlow emails the person a link of their own (signing up through it needs no confirmation: it proves the inbox). false: no invitation email, share the returned link yourself (signing up through it emails the person a confirmation first: an account is only created for a confirmed address)

Response

A CreatedInvitation.

14 fields of CreatedInvitation
  • uuidstring

    Its only id (a UUIDv7)

  • testbooleanOptional

    The mode of a member's invitation; absent for a team invitation (the team is mode-free)

  • emailstring
  • rolestring

    admin (the team) or user (a member)

  • holdFundsbooleanOptional

    Members: true = the organization holds their payments until it trusts them; false = paid directly. Absent for the team

  • organizationFeePercentnumberOptional

    Members: the organization's fee on their payments ("0": none). Written as organizationFeePercent. Absent for the team

  • redirectUrlstringOptional

    Where the person goes once they accepted, with ?invitationUuid=<uuid>. Absent when none

  • expiresAtstring
  • acceptedByUserUuidstringOptional

    The account that accepted; absent until accepted

  • acceptedAtstringOptional

    Absent until accepted

  • revokedAtstringOptional

    Absent unless revoked (or replaced by a newer one)

  • createdAtstring
  • statusstring

    Computed when read

    One of:

    • pendingSent, waiting for the person
    • acceptedAccepted: the person joined
    • revokedRevoked by the organization (or replaced by a newer one)
    • expiredNot accepted in time (TTL)
  • linkstring

    A link to accept, to share in your own onboarding if you prefer (sendEmail false: the only one). Signing up through it emails the person a confirmation first; the emailed link (its own token, never returned) doesn't. Only in this response

Errors

Also the common codes, as any route (what each means):

Get invitations#

GET /invitations

A page of the organization's invitations of the request's mode, newest first (default 20, at most 100), optionally with one status. From the dashboard, its team's too (an API key only sees member invitations: the team is managed from the dashboard).

Auth
X-API-Key: an admin key
On-Behalf-Of
Refused: the admin role is needed
Idempotency-Key
Ignored
Success
200
Request

request.sh

curl https://api.qbitflow.app/v2/invitations \  -H "X-API-Key: $QBITFLOW_API_KEY"# 200: a page of Invitation ({ items, hasMore, nextCursor? })
const page = await client.invitations.list({ status: InvitationStatus.Pending });for (const invitation of page.items) {	console.log(`${invitation.uuid} ${invitation.email}, expires ${invitation.expiresAt}`);}
page = client.invitations.list(status=qbitflow.InvitationStatus.PENDING)for invitation in page.items:    print(invitation.uuid, invitation.email, "expires", invitation.expires_at)
invitations, err := client.Invitations.List(ctx, &qbitflow.InvitationListParams{	Status: qbitflow.InvitationStatusPending,})if err != nil {	log.Fatal(err)}for _, inv := range invitations.Items {	fmt.Println(inv.UUID, inv.Email, "expires", inv.ExpiresAt.Format(time.RFC3339))}
$page = $client->invitations->list(new \QBitFlow\Params\InvitationListParams(status: \QBitFlow\Enums\InvitationStatus::PENDING));foreach ($page->items as $invitation) {	echo "{$invitation->uuid} {$invitation->email}, expires {$invitation->expiresAt->format('Y-m-d')}\n";}

Query parameters

  • limitintegerOptional

    Page size: 20 when absent or ≤ 0, at most 100 (a larger one is clamped, not refused)

  • cursorstringOptional

    The previous page's nextCursor, verbatim; omit for the first page

    max=200

  • statusstringOptional

    Only the invitations with this status

    oneof=pending accepted revoked expired

    One of:

    • pendingSent, waiting for the person
    • acceptedAccepted: the person joined
    • revokedRevoked by the organization (or replaced by a newer one)
    • expiredNot accepted in time (TTL)

Response

A page: { "items": Invitation[], "hasMore": …, "nextCursor"?: … }. Pass nextCursor back as cursor for the next page; it is absent when hasMore is false.

13 fields of each Invitation
  • uuidstring

    Its only id (a UUIDv7)

  • testbooleanOptional

    The mode of a member's invitation; absent for a team invitation (the team is mode-free)

  • emailstring
  • rolestring

    admin (the team) or user (a member)

  • holdFundsbooleanOptional

    Members: true = the organization holds their payments until it trusts them; false = paid directly. Absent for the team

  • organizationFeePercentnumberOptional

    Members: the organization's fee on their payments ("0": none). Written as organizationFeePercent. Absent for the team

  • redirectUrlstringOptional

    Where the person goes once they accepted, with ?invitationUuid=<uuid>. Absent when none

  • expiresAtstring
  • acceptedByUserUuidstringOptional

    The account that accepted; absent until accepted

  • acceptedAtstringOptional

    Absent until accepted

  • revokedAtstringOptional

    Absent unless revoked (or replaced by a newer one)

  • createdAtstring
  • statusstring

    Computed when read

    One of:

    • pendingSent, waiting for the person
    • acceptedAccepted: the person joined
    • revokedRevoked by the organization (or replaced by a newer one)
    • expiredNot accepted in time (TTL)

Revoke invitation#

DELETE /invitations/:uuid

Revokes a pending invitation: its link stops working. The team's are revoked from the dashboard only. Already accepted or revoked → 409. Answers 204.

Auth
X-API-Key: an admin key
On-Behalf-Of
Refused: the admin role is needed
Idempotency-Key
Ignored
Success
204
Request

request.sh

curl -X DELETE "https://api.qbitflow.app/v2/invitations/$INVITATION_UUID" \  -H "X-API-Key: $QBITFLOW_API_KEY"# 200: an Invitation
// Only before it is accepted: its link stops working (204, nothing returned).await client.invitations.revoke(invitationUuid);console.log(`revoked ${invitationUuid}`);
client.invitations.revoke(invitation_uuid)  # 204: its link stops workingprint("revoked", invitation_uuid)
// 204: its link stops working.if err := client.Invitations.Revoke(ctx, invitationUUID); err != nil {	log.Fatal(err) // 409 once accepted or revoked}fmt.Println("revoked", invitationUUID)
$client->invitations->revoke($invitationUuid); // a pending invitation only (204)echo "Revoked {$invitationUuid}: its link no longer works\n";

Path parameters

  • uuidstringRequired

Response

No JSON body: the edge cases below say what it answers.

Edge cases

The contract's rules for these routes: Invite — POST /invitations · List and revoke · The invited person · Several organizations per account · Test-mode members.

Read all 5

Invite — POST /invitations

  • Optional sendEmail (boolean, default true): QBitFlow emails the person a link of their own, in QBitFlow's name, stating a member's terms: who pays the fees (the organization's commission on the listed price when its checkout.customerPaysProcessingFee is on in the invitation's mode, buyers paying QBitFlow's fee on top; on what remains after QBitFlow's fee when it is off; without a commission, whether QBitFlow's fee comes out of their sales; network fees always the buyer's), then where their share goes (the percent they keep; held until the organization trusts them, or directly to their wallet). That link has its own token, never returned: only the email carries it, so signing up through it proves the inbox and needs no confirmation email. false sends no invitation email: show or send the returned link yourself (your own onboarding, your own email). The returned link works either way; a person signing up through it gets the sign-up confirmation email first (§ The invited person: you hold that link too, so an account is only created for a confirmed address). One with an account, or signing in with a provider, gets none. A non-boolean → 400 validation_failed on sendEmail.
  • A member (role: user) is invited in the request's mode. holdFunds is required: true holds their payments until the organization trusts them (POST /members/:userUuid/trust), false pays them directly. Optional organizationFeePercent (a string, "0" to "50", default "0").
  • A team invitation (role: admin) is mode-free, from the dashboard only (an API key → 403), without holdFunds or organizationFeePercent (→ 400 validation_failed on role). Its test, holdFunds and organizationFeePercent are absent (a member's invitation always has all three).
  • Optional redirectUrl (http_url): where the person goes once they accepted, with ?invitationUuid=<uuid> (it was ?invitationId=).
  • The invitation expires after 7 days. A new invitation of the same person and mode replaces the open one (its link stops working). Someone already there with that role and mode → 409 already_joined (a live member can still be invited in test).
  • holdFunds: true with organizationFeePercent > 0 (a marketplace's default): the member's payments go to the organization's wallet, the fee is withheld from what the organization owes them, and a release pays the net (Members and held funds § The organization fee on held funds). Held members sell subscriptions too. The organization fee is only taken on the chains where the organization has a wallet (and held funds need that wallet anyway).
  • A member → 403; validation → 400 validation_failed (email, role, holdFunds).
  • At most 50 invitations an hour per organization, both modes and every credential together (LimitPerOrganization(invitations)) → 429 rate_limit_exceeded past it, whether or not it emails the person. A caller refused for its role spends nothing.
  • Idempotency-Key (idempotency): a retried invitation with the same key answers the first one (CreatedInvitation, the same link) and sends no second email.

List and revoke

  • GET /invitations → 200 a page (newest first by createdAt; limit default 20, at most 100; cursor = the previous page's nextCursor) of the organization's invitations of the request's mode, and from the dashboard of its team too: an API key lists member invitations only (role: user), the team being managed from the dashboard. An accepted invitation names who accepted it with acceptedByUserUuid (their Member.userUuid) and acceptedAt; a revoked one has revokedAt (both absent otherwise). ?status=pending|accepted|revoked|expired filters. Another organization's never appear.
  • DELETE /invitations/:uuid (re-authentication) → 204 with no body: its link stops working (GET /invitations shows it revoked). Already accepted or revoked → 409 conflict; a team invitation with an API key → 403; another organization's or another mode's → 404.

The invited person

  • GET /invitations/preview/:token (public) → 200: organizationName, the invited email, role, test (absent for the team), status, expiresAt, and a member's terms, shown before they accept: holdFunds, organizationFeePercent and customerPaysProcessingFee (absent for the team). customerPaysProcessingFee is the organization's checkout.customerPaysProcessingFee in the invitation's mode when the link is opened (not when it was sent): true, buyers pay QBitFlow's fee on top of the price, so the commission is on the full listed price (a $100 sale at 20 %: $20 to the organization, $80 to the member); false, QBitFlow's fee comes out of the sale and the commission is on what remains. Members can't set that setting; a checkout's own fees.processingFee still decides for itself. Either link's token works (the returned one and the emailed one). A token that isn't one (not letters and digits, or longer than 128) → 400 validation_failed on token; a wrong token → 400 invalid_invitation.
  • POST /invitations/accept { "token" } with a session (dashboard-only): the signed-in account must be the invited address's (403 invitation_email_mismatch otherwise). → 200 the session re-signed where they joined (SpaceSession + redirectUrl): their member space of the invitation's mode, or the organization's live space for the team. A member invited to the team becomes an admin. An account that already belongs to other organizations joins this one too (§ Several organizations per account).
  • An account removed from its organization (no membership left) signs in without an organization (Account and credential § Sessions) and accepts with POST /invitations/accept (its session reaches it), or signs in with the invitation's token: POST /auth/login { email, password, invitationToken } → the Login where they joined, with redirectUrl. The invited address must be the account's (403 invitation_email_mismatch). Their former personal space reopens with this invitation's terms, their data still there.
  • There is no list of the invitations an account received: the link (its token) is how one is previewed and accepted.
  • A member joining (accept or sign-up confirmation) publishes member.joined to the organization's endpoints, in the mode they joined (Webhook events): the marketplace learns who joined, with their userUuid, without polling. A team join publishes none.
  • POST /invitations/sign-up { token } (public) → 200 SignUpRequested (message, and confirmationToken on the emailed link's path):
    • Through the emailed link (its token only ever reached the invited inbox): a free address gets confirmationToken at once, no second email. Post it to …/sign-up/confirm with the account's name and password.
    • Through the returned link (the organization holds it too, so it doesn't prove owning the address): a free address gets a confirmation link by email (/invitations/sign-up/confirm?token=, valid 24 hours, subject "Confirm your email to join <organization> on QBitFlow"), and no confirmationToken.
    • An address with an account, through either link: an email saying so (sign in and accept), and no confirmationToken, the same answer as an emailed confirmation.

The account is only created on confirmation. Capped like every confirmation email (errors): 5 an hour per invitation, 20 an hour per client IP → 429 rate_limit_exceeded.

  • POST /invitations/sign-up/confirm { token, firstName, lastName, password } (the token: the confirmationToken, or the confirmation email's; whoever proved the address chooses their name and password) → 201 the Login of the new account where they joined, with redirectUrl. A wrong, expired or used confirmation → 400 invalid_confirmation_token.
  • An invitation expired, revoked or already accepted → 400 invalid_invitation.
  • An invitation of a closed organization → 400 invalid_invitation: closing an organization revokes its pending invitations, and an acceptance (accept or sign-up confirmation) never brings anyone or any space back into a closed organization, even for an invitation left pending.

Several organizations per account

  • An account may belong to several organizations, with a role in each, and own several. Accepting an invitation (/accept, or a login with invitationToken) with an account that is already an active member, admin or owner of another organization succeeds like any acceptance: it keeps its other memberships, and its session moves to where it joined. A seller who already uses QBitFlow joins your platform with the same account.
  • Its spaces in every organization are listed by GET /account/spaces (and Login.spaces), and its session switches between them (POST /auth/switch-space, Account and credential § Sessions). Each session acts in one space: one organization's data never shows in another's.
  • An account removed from its organization belongs to none: it accepts normally (§ The invited person).

Test-mode members

  • A test member is a real account too: invited by a test key (or the dashboard in test mode), they accept from a real inbox (the confirmation email) and their test space is theirs. There is no way yet to create accepted test members from a key (for CI); use real addresses you control (you+seller1@yourdomain.com), invited once and kept.

The codes named here