Skip to main content

Payment API

Complete GraphQL reference for all billing operations: plans, subscriptions, credits, invoices, promo codes, and admin operations.

Plans

List Plans

query {
plans {
id name price currency interval active billingMode
creditsPerMonth trialDays sortOrder
maxJobs maxTalentPools maxContacts maxPipelines maxCompanies
maxInterviewsPerMonth maxMeetingsPerMonth
maxCvCollections maxSharedTalentLists
maxCollaborationInvites maxConnectionRequestsPerMonth
maxUsersPerCompany maxNewsPerMonth maxContactNotes
maxCvParsesPerMonth maxAgentMessagesPerMonth maxTextRewritesPerMonth
}
}

Auth: JWT required.

List Public Plans (anonymous)

For the public pricing page — no JWT needed. The publicPlans resolver has no JwtAuthGuard and exposes the same plan shape as plans minus timestamps.

query GetPublicPlans {
publicPlans {
id name description price currency interval
active billingMode trialDays sortOrder creditsPerMonth
maxJobs maxTalentPools maxContacts maxPipelines
maxInterviewsPerMonth maxMeetingsPerMonth
maxCvCollections maxSharedTalentLists
maxCollaborationInvites maxConnectionRequestsPerMonth
maxUsersPerCompany maxNewsPerMonth maxContactNotes
maxCvParsesPerMonth maxAgentMessagesPerMonth maxTextRewritesPerMonth
maxCompanies
features { name description included highlight }
}
}

Auth: none — designed for anonymous visitors on /pricing. The authenticated plans query requires a JWT and will 401 on public pages; always use publicPlans from anonymous contexts.

Get Single Plan

query {
plan(id: "plan-uuid") {
id name price currency interval creditsPerMonth
}
}

Plan Mutations (SuperAdmin)

mutation { createPlan(input: { name: "Pro", price: 29.99, currency: "usd", interval: "month" }) { id name } }
mutation { updatePlan(id: "uuid", input: { price: 39.99, active: true }) { id price active } }
mutation { removePlan(id: "uuid") { success message } }
Stripe Auto-Sync

When a paid PREPAID plan is created or updated, the backend automatically creates a Stripe Product and recurring Price. The stripePriceId and stripeProductId fields are populated automatically — no manual configuration needed.

Plan Stripe Sync (SuperAdmin)

Sync Stripe pricing for plans that are missing it (e.g., plans created before auto-sync was added):

# Sync a single plan
mutation { syncPlanStripePricing(id: "plan-uuid") { id stripePriceId stripeProductId stripeConfigured } }

# Sync all plans
mutation { syncAllPlansStripePricing { id name stripePriceId stripeProductId stripeConfigured } }

Plan Type Fields

FieldTypeDescription
stripePriceIdStringStripe recurring Price ID (null for free/POSTPAID plans)
stripeProductIdStringStripe Product ID (null for free/POSTPAID plans)
stripeConfiguredBoolean!true if the plan has Stripe pricing or doesn't need it
CreatePlanInput accepts the full plan shape

CreatePlanInput takes name, price, currency, interval, plus optional description, active, billingMode, trialDays, sortOrder, creditsPerMonth, all 16 feature limits (maxJobs, maxTalentPools, …), and a features: [PlanFeatureInput!] array for pricing-card bullets. Price is validated with @Min(0)price: 0 is accepted for POSTPAID (enterprise) plans and free-tier PREPAID plans. UpdatePlanInput extends PartialType(CreatePlanInput), so the same fields are settable on updates.

Plan.price is in whole currency units, not cents

Plan.price=24 means €24.00, not 24 cents. The backend multiplies by 100 when creating the Stripe Price (unit_amount = price * 100). Never pre-multiply on the frontend or divide on the display side — Plan.price is already the human-readable value. See Payment System → Plan.price Unit Convention for the 100× incident that shipped this warning.

Subscriptions

Active Subscription

query {
activeSubscription {
id status startDate endDate trialStart trialEnd
companyId subscribedById stripeSubscriptionId
plan { id name price currency interval creditsPerMonth }
}
}

Returns null if the company has no active subscription.

Subscription History

query {
subscriptionHistory {
id status startDate endDate createdAt
plan { name price interval }
}
}

Returns an array ordered newest first.

Start Subscription Checkout

mutation {
startSubscriptionCheckout(
planId: "plan-uuid"
companyId: "company-uuid" # optional, defaults to selectedCompanyId
promoCodeId: "promo-uuid" # optional, applies Stripe coupon
)
# Returns: Stripe checkout URL (String)
}

Auth: JWT required. companyId must match user.selectedCompanyId.

Cancel Subscription

mutation {
cancelSubscription(id: "sub-uuid", input: {
cancelAtPeriodEnd: true # false = immediate cancellation
feedback: "too_expensive" # optional
comment: "Budget constraints" # optional
prorate: false # credit remaining time (immediate only)
}) {
success message
}
}

Feedback values: customer_service, low_quality, missing_features, other, switched_service, too_complex, too_expensive, unused

Credit Balance

Get Balance

query {
creditBalance {
id balance totalEarned totalConsumed
lastReplenishedAt createdAt updatedAt
}
}

Routing: EMPLOYER mode returns company balance; JOB_SEEKER mode returns personal balance. Fields balance, totalEarned, totalConsumed are Float in GraphQL, backed by Decimal(10,6).

Transaction History

query {
creditTransactions(filter: {
type: CONSUMPTION # optional: PLAN_ALLOCATION | PACK_PURCHASE | CONSUMPTION | MANUAL_ADJUSTMENT | INITIAL_GRANT | REFUND | TRANSFER
take: 20 # page size (default 20, max 100)
skip: 0 # offset
}) {
id type amount balanceAfter description
referenceId referenceType metadata createdAt
}
}

Transfer Credits

mutation {
transferCredits(input: {
fromCompanyId: "company-uuid" # optional — omit to debit from the caller's personal balance
toCompanyId: "company-uuid" # optional — omit to credit the caller's personal balance
amount: 100 # min: 0.01
}) {
success message
}
}

Both fromCompanyId and toCompanyId are optional; at least one must be set and they must differ. This gives three meaningful directions: personal→company (existing), company→company, and company→personal.

Auth: When the source is a company, the caller must be the billing owner of that source company. For the legacy personal→company case (no fromCompanyId), the caller must be the billing owner of the target company. Creates two TRANSFER transaction records — one debit, one credit — inside a single transaction with pessimistic locking on both balances.

Credit Packs

List Available Packs

query {
creditPacks {
id name description credits price currency sortOrder
}
}

Auth: Not required (public endpoint). Only ACTIVE packs are returned, sorted by sortOrder.

Purchase Credit Pack

mutation {
purchaseCreditPack(input: {
creditPackId: "pack-uuid"
companyId: "company-uuid" # optional, defaults to selectedCompanyId
})
# Returns: Stripe checkout URL (String)
}

Credits land in the user's personal balance. Use transferCredits to move them to a company.

Plan Usage Summary

query {
planUsageSummary {
planName
limits {
feature # e.g., "maxJobs"
limit # -1 = unlimited, 0 = disabled, >0 = cap
currentUsage # current count
isMonthly # resets on 1st of month
isUnlimited # true when limit is -1
}
}
}

Auth: JWT required. Must have a company selected.

Invoices

List Invoices

query {
invoices {
id amount currency status stripeInvoiceId createdAt
}
}

Invoice PDF URL

query {
invoicePdfUrl(stripeInvoiceId: "in_xxx")
# Returns: String (download URL)
}

Create Manual Invoice

mutation {
createManualInvoice(userId: "user-uuid", input: {
subscriptionId: "sub-uuid"
amount: 99.99
currency: "usd"
description: "Manual adjustment"
}) {
id amount status
}
}

Promo Codes

Create Promo Code

mutation {
createPromoCode(input: {
code: "SAVE20"
discountType: PERCENTAGE # or FIXED_AMOUNT
discountValue: 20
usageLimit: 100
expiresAt: "2026-12-31T23:59:59Z"
applicablePlanIds: [] # empty = all plans
requiresInvitation: false
}) {
id code discountType discountValue remainingUses canBeUsed
}
}

Redeem Promo Code

mutation {
redeemPromoCode(input: { code: "SAVE20", planId: "plan-uuid" }) {
id discountApplied currency
PromoCode { code discountType discountValue }
Plan { name price }
}
}

Promo Code Queries

query { myPromoCodes { id code usageCount remainingUses isExpired canBeUsed } }
query { promoCode(id: "uuid") { id code Redemptions { userId discountApplied } } }
query { myPromoCodeStatistics { totalCodes activeCodes totalRedemptions totalDiscountGiven } }

Invitation Operations

mutation {
inviteToPromoCode(input: {
promoCodeId: "promo-uuid"
invitedEmail: "friend@example.com"
message: "Try this discount!"
}) { id status token expiresAt }
}

mutation {
acceptPromoCodeInvitation(token: "secure-token") {
id status acceptedAt
PromoCode { code discountValue }
}
}

query { mySentPromoCodeInvitations { id invitedEmail status acceptedAt } }
query { myReceivedPromoCodeInvitations { id status token Inviter { email } PromoCode { code } } }

Billing Config

Get Billing Configs

query {
myBillingConfigs {
id companyId creditAllocationPercent
allocatedMaxJobs allocatedMaxTalentPools allocatedMaxContacts
# ... all 16 allocated* fields
createdAt updatedAt
}
}

Upsert Billing Config

mutation {
upsertBillingConfig(input: {
companyId: "company-uuid"
creditAllocationPercent: 60.0
allocatedMaxJobs: 8 # override plan default
}) {
id companyId creditAllocationPercent allocatedMaxJobs
}
}

Auth: Must be the billing owner. Total creditAllocationPercent across all companies cannot exceed 100%.

AI Operations

Operation Logs

query {
aiOperationLogs(filter: {
operationType: CV_EXTRACTION # CV_EXTRACTION | JOB_PARSING | MEETING_INSIGHT | TEXT_REWRITE | AGENT_CHAT | FACE_EXTRACTION | MATCHING | EMBEDDING | MEETING_TRANSCRIPTION | VOICE_CHAT
startDate: "2026-01-01"
endDate: "2026-02-01"
limit: 50
offset: 0
}) {
id companyId userId operationType modelId
inputTokens outputTokens totalTokens creditsCost
status durationMs metadata createdAt
}
}

Usage Summary

query {
aiOperationUsageSummary(companyId: "company-uuid", startDate: "2026-01-01", endDate: "2026-02-01") {
operationType totalOperations totalTokens totalCredits
}
}

Credit Cost Configs

query { activeCreditCostConfigs { modelId inputCostPer1k outputCostPer1k } }

Admin Operations (SuperAdmin)

All admin mutations require JwtAuthGuard + SuperAdminGuard.

Grant Credits

mutation {
adminGrantCredits(input: {
companyId: "company-uuid" # XOR with userId
amount: 500
description: "Bonus credits"
}) {
id balance totalEarned
}
}

Exactly one of companyId or userId must be provided. Creates a MANUAL_ADJUSTMENT transaction.

Assign Subscription

mutation {
adminAssignSubscription(input: {
companyId: "company-uuid"
planId: "plan-uuid"
}) {
id status plan { name creditsPerMonth }
}
}

Deactivates existing subscriptions, creates a new ACTIVE subscription without Stripe. Does not trigger subscription.created (credits are not auto-granted -- use adminGrantCredits separately).

Resolves the billing owner as Company.billingOwnerId ?? earliest Company Admin ?? the admin caller, and writes it to both the subscription row (userId + subscribedById) and to Company.billingOwnerId when that field is still NULL. This ensures the resulting subscription is reachable by the plan-limit lookups on the next request.

Backfill Company Billing Owners

mutation { adminBackfillCompanyBillingOwners }

Returns Int — the number of rows updated. Idempotent: the mutation runs a single UPDATE that only touches companies where billingOwnerId IS NULL, setting each to the earliest user holding the Company Admin role on that company. Safe to re-run; returns 0 when nothing needs fixing.

Companies with no Company Admin role holder at all are not touched — they need a role assignment first, which itself auto-populates billingOwnerId as part of the create (see Credit System → Subscription Resolution).

Credit Pack CRUD

mutation { createCreditPack(input: { name: "Starter", credits: 50, price: 9.99, currency: "usd" }) { id stripePriceId } }
mutation { updateCreditPack(id: "uuid", input: { price: 14.99 }) { id price stripePriceId } }
mutation { removeCreditPack(id: "uuid") { success message } }

Stripe Products and Prices are auto-created. Updating price or currency creates new Stripe objects (prices are immutable in Stripe).

Credit Cost Config CRUD

mutation { createCreditCostConfig(input: { modelId: "model-id", displayName: "Model", inputCostPer1k: 0.005, outputCostPer1k: 0.025 }) { id } }
mutation { updateCreditCostConfig(id: "uuid", input: { inputCostPer1k: 0.004 }) { id } }
mutation { deleteCreditCostConfig(id: "uuid") { id isActive } }

Admin AI Operation Logs

query { adminAIOperationLogs(filter: { companyId: "uuid" }) { id companyId operationType status createdAt } }

Enterprise Billing (SuperAdmin)

Enterprise billing operations for post-paid (POSTPAID) subscriptions. See Enterprise Post-Paid Billing for full architecture details.

Enterprise Mutations

# Admin creates enterprise subscription (immediate ACTIVE)
mutation {
adminCreateEnterpriseSubscription(input: {
companyId: "company-uuid"
planId: "enterprise-plan-uuid"
billingOwnerId: "user-uuid"
}) { id status isActive Plan { name billingMode } }
}

# User requests enterprise subscription (PENDING_APPROVAL)
mutation {
requestEnterpriseSubscription(input: {
companyId: "company-uuid"
planId: "enterprise-plan-uuid"
}) { id status isActive }
}

# Approve/reject requests
mutation { adminApproveEnterpriseSubscription(input: { subscriptionId: "uuid", billingOwnerId: "uuid" }) { id status } }
mutation { adminRejectEnterpriseSubscription(subscriptionId: "uuid", reason: "Does not qualify") { id status } }

# Access control — scope is XOR: exactly one of companyId or userId
mutation { adminBlockEnterpriseAccess(input: { companyId: "uuid" }) { id status isActive } }
mutation { adminBlockEnterpriseAccess(input: { userId: "uuid" }) { id status isActive } } # personal
mutation { adminRestoreEnterpriseAccess(input: { companyId: "uuid" }) { id status isActive } }
mutation { adminRestoreEnterpriseAccess(input: { userId: "uuid" }) { id status isActive } } # personal

# Invoice management — companyId or userId; one must be provided
mutation { adminGenerateEnterpriseInvoice(input: { companyId: "uuid", startDate: "...", endDate: "..." }) { id amount status dueDate } }
mutation { adminGenerateEnterpriseInvoice(input: { userId: "uuid", startDate: "...", endDate: "..." }) { id amount status dueDate } }
mutation { adminMarkInvoicePaid(invoiceId: "uuid") { success message } }

Input types:

input CreateEnterpriseSubscriptionInput {
companyId: ID # Optional — omit for a personal enterprise subscription
planId: ID!
billingOwnerId: ID!
}

input EnterpriseAccessScopeInput {
companyId: ID # One of
userId: ID # the two — exactly one must be provided
}

input GenerateEnterpriseInvoiceInput {
companyId: ID # One of
userId: ID # the two
startDate: DateTime!
endDate: DateTime!
}

Enterprise Queries

# List all enterprise subscriptions (company-scoped and personal)
query {
adminEnterpriseSubscriptions {
id status isActive companyId userId
Plan { name billingMode }
Company { companyName }
SubscribedBy { id email firstName lastName }
}
}

# Usage breakdown for invoicing preview — companyId or userId (XOR)
query {
adminEnterpriseUsageBreakdown(companyId: "uuid", startDate: "...", endDate: "...") {
companyId companyName userId userName totalAmount currency
lineItems { operationType operationCount totalCost totalInputTokens totalOutputTokens }
}
}

# Personal-scope usage
query {
adminEnterpriseUsageBreakdown(userId: "uuid", startDate: "...", endDate: "...") {
userId userName totalAmount currency
lineItems { operationType operationCount totalCost }
}
}

EnterpriseUsageBreakdown has nullable companyId / companyName (null for personal) plus userId / userName (null for company-scoped).

Auth: All admin mutations require JwtAuthGuard + SuperAdminGuard, except requestEnterpriseSubscription which is available to any authenticated user.

Personal enterprise subscriptions

A subscription without a company (Subscription.companyId IS NULL) is owned by the billing-owner user. All admin mutations and queries listed above support both shapes; provide companyId for company-scoped subs or userId for personal subs. See Enterprise Post-Paid Billing for the full lifecycle.

Error Codes

CodeHTTPDescription
PLAN_LIMIT_EXCEEDED403Feature usage at plan limit
INSUFFICIENT_CREDITS402Balance too low for operation
CREDIT_BALANCE_NOT_FOUND404No balance record found
CREDIT_PACK_NOT_FOUND404Invalid credit pack ID
CREDIT_PACK_INACTIVE400Attempting to purchase inactive pack
NO_PLAN_FOUND404Company has no active subscription
NOT_FOUND404Plan, subscription, invoice, or promo code not found
FORBIDDEN403Wrong company context or not the resource owner