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 } }
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
| Field | Type | Description |
|---|---|---|
stripePriceId | String | Stripe recurring Price ID (null for free/POSTPAID plans) |
stripeProductId | String | Stripe Product ID (null for free/POSTPAID plans) |
stripeConfigured | Boolean! | true if the plan has Stripe pricing or doesn't need it |
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=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.
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
| Code | HTTP | Description |
|---|---|---|
PLAN_LIMIT_EXCEEDED | 403 | Feature usage at plan limit |
INSUFFICIENT_CREDITS | 402 | Balance too low for operation |
CREDIT_BALANCE_NOT_FOUND | 404 | No balance record found |
CREDIT_PACK_NOT_FOUND | 404 | Invalid credit pack ID |
CREDIT_PACK_INACTIVE | 400 | Attempting to purchase inactive pack |
NO_PLAN_FOUND | 404 | Company has no active subscription |
NOT_FOUND | 404 | Plan, subscription, invoice, or promo code not found |
FORBIDDEN | 403 | Wrong company context or not the resource owner |