CRM & Loyalty
Manage customer contacts, marketing campaigns, loyalty programs (tiers, rewards, and redemptions), and the affiliate/referral network. These endpoints power merchant retention and growth features.
Every CRM endpoint requires a valid JWT or API key sent as Authorization: Bearer <token>. The base
URL for all requests is https://gullypay.xyz/api. Contact, campaign, and loyalty resources are scoped
to a merchant via the :merchantId path segment; loyalty sub-resources additionally verify that the
program belongs to that merchant.
Endpoints
Contacts & Campaigns
/merchants/:merchantId/contactsCreate a contact (auth required)/merchants/:merchantId/contactsList contacts for a merchant (auth required)/merchants/:merchantId/campaignsCreate a marketing campaign (auth required)/merchants/:merchantId/campaignsList campaigns for a merchant (auth required)/merchants/:merchantId/campaigns/:idCampaign status with recipient breakdown (auth required)/merchants/:merchantId/campaigns/:id/sendSend/dispatch a campaign to its audience (auth required)Automations
/merchants/:merchantId/automationsCreate a marketing automation (auth required)/merchants/:merchantId/automationsList automations for a merchant (auth required)/merchants/:merchantId/automations/:idUpdate / pause / activate an automation (auth required)/merchants/:merchantId/automations/:idDelete an automation (auth required)Loyalty
/merchants/:merchantId/loyaltyCreate a loyalty program (auth required)/merchants/:merchantId/loyaltyList programs with member/point stats (auth required)/merchants/:merchantId/loyalty/summaryLoyalty dashboard summary (auth required)/merchants/:merchantId/loyalty/:programId/tiersCreate a tier (auth required)/merchants/:merchantId/loyalty/:programId/tiersList tiers with member counts (auth required)/merchants/:merchantId/loyalty/:programId/rewardsCreate a reward (auth required)/merchants/:merchantId/loyalty/:programId/rewardsList rewards catalog (auth required)/merchants/:merchantId/loyalty/:programId/redemptionsRedeem a reward for a member (auth required)/merchants/:merchantId/loyalty/:programId/redemptionsList recent redemptions (auth required)Affiliates & Referrals
/merchants/:merchantId/affiliatesAffiliate leaderboard for a merchant (auth required)/merchants/:merchantId/referralsReferral funnel aggregates (auth required)/merchants/:merchantId/affiliate-payablesPer-affiliate owed commission (auth required)/merchants/:merchantId/affiliate-payoutsCommission payout history (auth required)/merchants/:merchantId/affiliate-payoutsPay out all affiliates with a balance owed (auth required)/merchants/:merchantId/affiliates/:affiliateId/payoutsPay out a single affiliate (auth required)/affiliatesCreate an affiliate for the authenticated user (auth required)/affiliates/:affiliateId/referralsTrack a referral (auth required)Contacts store customer profiles with free-form tags — there is no separate segment resource;
segmentation is done by filtering on tags (the campaign send endpoint accepts an optional tag to
narrow the audience). Campaigns can now be sent: sending snapshots the target audience into
campaign_recipients rows and transitions the campaign to sent.
Sending a campaign or running an automation persists the target audience as real campaign_recipients
rows with status queued and records the real audience size — it does not hand messages to a live
email/SMS provider. Actual transport is a provider swap (the same pattern as the Stripe mock): recipients
stay queued and are never reported as delivered. openCount/clickCount remain a real 0 until a
provider reports delivery events. Wire a transport provider to flip recipient status and increment those
counters.
Contacts
/merchants/:merchantId/contactsCreate a contact| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Contact email |
name | string | No | Contact name |
phone | string | No | Contact phone number |
tags | string[] | No | Free-form tags used for segmentation |
GET /merchants/:merchantId/contacts returns all contacts for the merchant.
Campaigns
/merchants/:merchantId/campaignsCreate a marketing campaign| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Campaign name |
type | string | Yes | Campaign type, e.g. email, sms |
subject | string | No | Subject line |
body | string | No | Campaign body content |
Send a Campaign
/merchants/:merchantId/campaigns/:id/sendSend/dispatch a campaign to its audienceSnapshots the merchant's contacts (optionally filtered by tag) into campaign_recipients rows with
status queued, transitions the campaign to sent, sets sentAt, and records the real audience size
as sentCount. Returns 400 if the campaign was already sent or if no contacts match.
| Field | Type | Required | Description |
|---|---|---|---|
tag | string | No | Only queue contacts whose tags contain this value; omit to target all contacts |
GET /merchants/:merchantId/campaigns/:id returns the campaign plus a real recipientCount and a
recipientsByStatus breakdown (e.g. { "queued": 128 }).
Automations
Trigger-driven customer journeys. trigger is one of abandoned_cart, welcome, post_purchase,
win_back; status is active, paused, or draft; actionType is email or sms.
/merchants/:merchantId/automationsCreate an automation| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Automation name |
trigger | string | Yes | One of abandoned_cart, welcome, post_purchase, win_back |
actionType | string | No | email or sms (default: email) |
status | string | No | active, paused, or draft (default: active) |
config | object | No | Message template/settings payload |
GET /merchants/:merchantId/automations lists automations newest-first, each with real runCount and
lastRunAt counters (start at 0/null).
/merchants/:merchantId/automations/:idUpdate / pause / activate an automationAccepts any subset of name, status, actionType, trigger, config. Pausing is
{ "status": "paused" }; re-activating is { "status": "active" }. Returns 400 if the automation
does not belong to the merchant.
/merchants/:merchantId/automations/:idDelete an automationLoyalty Programs
/merchants/:merchantId/loyaltyCreate a loyalty program| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Program name |
pointsPerDollar | number | No | Points earned per dollar spent |
GET /merchants/:merchantId/loyalty returns each program enriched with real member and point stats:
Loyalty Summary
/merchants/:merchantId/loyalty/summaryLoyalty dashboard summaryAggregates all of a merchant's programs into a dashboard payload, computed entirely from real point
balances. pointsRedeemed is derived as lifetime-earned minus currently-outstanding points.
Tiers
/merchants/:merchantId/loyalty/:programId/tiersCreate a tier| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Tier name |
minPoints | number | No | Minimum point balance to reach this tier (default: 0) |
sortOrder | number | No | Display order (default: 0) |
perks | object | No | Arbitrary perks payload |
GET .../tiers returns tiers ordered by sortOrder then minPoints, each with a real memberCount
computed by bucketing each member's current balance into the highest tier they qualify for.
Rewards
/merchants/:merchantId/loyalty/:programId/rewardsCreate a reward| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Reward name |
description | string | No | Reward description |
pointsCost | number | Yes | Point cost — must be a positive integer |
active | boolean | No | Whether the reward is redeemable (default: true) |
GET .../rewards returns the catalog ordered by ascending pointsCost.
Redemptions
/merchants/:merchantId/loyalty/:programId/redemptionsRedeem a reward for a member| Field | Type | Required | Description |
|---|---|---|---|
userId | string | Yes | Member redeeming the reward |
rewardId | string | Yes | Reward to redeem |
The redemption runs in a single transaction with a row lock: it verifies the reward is active and
belongs to the program, checks the member holds enough points, deducts the cost, and records the
redemption. Insufficient balance, an inactive reward, or a member with no balance returns 400.
GET .../redemptions returns recent redemptions joined to reward name and redeeming user.
Affiliates & Referrals
Create an Affiliate
/affiliatesCreate an affiliate for the authenticated user| Field | Type | Required | Description |
|---|---|---|---|
code | string | Yes | Unique referral code |
commissionRate | number | No | Commission percentage (default: 10) |
Track a Referral
/affiliates/:affiliateId/referralsTrack a referral| Field | Type | Required | Description |
|---|---|---|---|
referredUserId | string | Yes | The referred user |
commission | number | Yes | Commission amount attributed to this referral |
Affiliate Leaderboard
/merchants/:merchantId/affiliatesAffiliate leaderboard for a merchantReturns each affiliate enriched with a real referral count and summed commission, sorted by total earned (highest first).
Referral Stats
/merchants/:merchantId/referralsReferral funnel aggregatesCommission Payouts
Each affiliate's owed balance is derived from the real referral ledger:
owed = SUM(referrals.commission) − affiliates.totalPaid (never negative). Paying out records an
affiliate_payouts row and increments the affiliate's totalPaid so the owed balance zeroes out —
amounts are always computed from real data, never fabricated.
/merchants/:merchantId/affiliate-payablesPer-affiliate owed commission/merchants/:merchantId/affiliates/:affiliateId/payoutsPay out a single affiliateSettles one affiliate's owed balance in a transaction (row-locked). Returns 400 if nothing is owed.
/merchants/:merchantId/affiliate-payoutsPay out all affiliates with a balance owedPays out every affiliate whose owed > 0. Returns 400 if none have a balance.
/merchants/:merchantId/affiliate-payoutsCommission payout historyReturns recent payouts (newest first) joined to the affiliate code.