import {z} from 'zod' export type CreateApiSaleArgs = {'Account-ID': string, body: {email: string, uid?: string | undefined, event_id: string, amount: number, quantity?: number | undefined, plan?: string | undefined, currency?: string | undefined, mrr?: string | undefined, promo_code?: string | undefined, tid?: string | undefined, ref_id?: string | undefined, skip_email_notification?: boolean | undefined} | {email?: string | undefined, uid: string, event_id: string, amount: number, quantity?: number | undefined, plan?: string | undefined, currency?: string | undefined, mrr?: string | undefined, promo_code?: string | undefined, tid?: string | undefined, ref_id?: string | undefined, skip_email_notification?: boolean | undefined}}; export type CreateApiSaleOptions = {baseUrl?: string | undefined, BearerAuth?: string | undefined, fetch?: typeof fetch | undefined}; export const createApiSaleResponse = z.object({id: z.number().int().optional(), etype: z.string().optional(), sale_amount: z.number().int().optional(), original_sale_amount: z.number().int().optional(), original_sale_currency: z.string().nullable().optional(), event_id: z.string().nullable().optional(), plan_id: z.string().nullable().optional(), billing_period: z.string().nullable().optional(), created_at: z.string().optional(), referral: z.object({id: z.number().int().optional(), email: z.string().optional(), uid: z.string().nullable().optional()}).optional(), commissions: z.array(z.object({id: z.number().int().optional(), status: z.string().optional(), metadata: z.object({}).optional(), is_self_referral: z.boolean().optional(), commission_type: z.string().optional(), created_by_user_email: z.string().nullable().optional(), created_by_user_at: z.string().nullable().optional(), sale_amount: z.number().int().optional(), original_sale_amount: z.number().int().optional(), original_sale_currency: z.string().nullable().optional(), event_id: z.string().nullable().optional(), plan_id: z.string().nullable().optional(), tier: z.number().int().optional(), internal_note: z.string().nullable().optional(), external_note: z.string().nullable().optional(), unit: z.string().optional(), fraud_check: z.string().optional(), amount: z.number().int().optional(), is_paid: z.boolean().optional(), is_split: z.boolean().optional(), created_at: z.string().optional(), status_updated_at: z.string().nullable().optional(), promoter_campaign: z.object({id: z.number().int().optional(), campaign_id: z.number().int().optional(), promoter_id: z.number().int().optional(), created_at: z.string().optional(), promoter: z.object({id: z.number().int().optional(), email: z.string().optional(), name: z.string().optional()}).optional(), campaign: z.object({id: z.number().int().optional(), name: z.string().optional(), color: z.string().optional()}).optional()}).optional(), referral: z.object({id: z.number().int().optional(), email: z.string().optional(), uid: z.string().nullable().optional()}).optional(), reward: z.object({id: z.number().int().optional(), name: z.string().optional()}).optional()})).optional()}); export const validateResponse = async ( schema: { readonly '~standard': { readonly validate: (value: unknown) => ({ readonly value: Output; readonly issues?: undefined } | { readonly issues: ReadonlyArray<{ readonly message: string; readonly path?: ReadonlyArray | undefined }> }) | Promise<{ readonly value: Output; readonly issues?: undefined } | { readonly issues: ReadonlyArray<{ readonly message: string; readonly path?: ReadonlyArray | undefined }> }> } }, value: unknown ): Promise => { const result = await schema['~standard'].validate(value) if (result.issues !== undefined) { const detail = result.issues .map(({ message, path }) => path?.length ? `${path.map((segment) => String(typeof segment === 'object' ? segment.key : segment)).join('.')}: ${message}` : message ) .join('; ') throw new Error(`Response does not match the schema: ${detail}`, { cause: { issues: result.issues } }) } return result.value }; /** * Assign sales and commissions to your promoters. To track sales and generate commissions correctly, you need to use this API call each time a non-zero amount sale is processed in your system, even if it comes from a recurring charge or one-time charge. To avoid fraudulent sales, we don't use JS conversion pixels to track sales, which are very unreliable and can be easily faked. Instead, we use server-side tracking for all sales to ensure that a sale is tracked only when you actually receive money in your billing account.

To maintain the same standards with the API, we recommend making the sale API call (this call) only when you receive confirmation of the sale from your billing provider, such as from a webhook, an IPN, or a success response from an API charge call.

You just need to pass the sale amount (before taxes) and we'll take care of the rest. The commissions/rewards will be calculated based on that amount and the plan ID, in case you use the plan-level rewards feature.
Using email or UID parameters, we identify the lead/customer who generated the sale, which also helps us determine the promoter who owns the reward/commission.

The lead is added to our system either by the client signup tracking script when the user signs up or by calling the signup API endpoint. There is also the option to bypass the signup tracking by using TID or ref_id parameters, which will create the lead and assign the sale in one go.

**If we don't find the lead in our system, then it means that the sale is not a referral sale, you'll get a 404 response. Also, if the promoter is banned, you'll get a 404 response. You don't have to identify which sale is from referrals and which is not; we'll take care of that.** **For zero-decimal** currencies like `JPY`, `amount` and `mrr` parameters should be sent as **whole values**.

**For other currencies**, `amount` and `mrr` parameter values should be in cents, i.e., you will need to **multiply the value by 100** before sending the request.
**HTTP Request**
`POST https://api.firstpromoter.com/api/v2/track/sale`
* * POST /sale */ export const createApiSale = async (args: CreateApiSaleArgs, options?: CreateApiSaleOptions) => { const {baseUrl = "https://api.firstpromoter.com/api/v2/company", fetch: fetchFn = fetch, BearerAuth} = options ?? {} const query = new URLSearchParams() const headers = new Headers() headers.set('content-type', "application/json") headers.set('accept', "application/json") if (args['Account-ID'] != null) headers.set("Account-ID", String(args['Account-ID'])) if (BearerAuth !== undefined) headers.set('authorization', `Bearer ${BearerAuth}`) const url = `${baseUrl.replace(/\/+$/, '')}/sale${query.size > 0 ? `?${query}` : ''}` const res = await fetchFn(url, {method: "POST", headers, body: JSON.stringify(args.body)}) if (!res.ok) throw new Error(`Request failed with status ${res.status}`, {cause: {status: res.status, body: await res.text()}}) return validateResponse(createApiSaleResponse, await res.json()) };