Docs
Dashboard

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.

Authentication

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

POST/merchants/:merchantId/contactsCreate a contact (auth required)
GET/merchants/:merchantId/contactsList contacts for a merchant (auth required)
POST/merchants/:merchantId/campaignsCreate a marketing campaign (auth required)
GET/merchants/:merchantId/campaignsList campaigns for a merchant (auth required)
GET/merchants/:merchantId/campaigns/:idCampaign status with recipient breakdown (auth required)
POST/merchants/:merchantId/campaigns/:id/sendSend/dispatch a campaign to its audience (auth required)

Automations

POST/merchants/:merchantId/automationsCreate a marketing automation (auth required)
GET/merchants/:merchantId/automationsList automations for a merchant (auth required)
PATCH/merchants/:merchantId/automations/:idUpdate / pause / activate an automation (auth required)
DELETE/merchants/:merchantId/automations/:idDelete an automation (auth required)

Loyalty

POST/merchants/:merchantId/loyaltyCreate a loyalty program (auth required)
GET/merchants/:merchantId/loyaltyList programs with member/point stats (auth required)
GET/merchants/:merchantId/loyalty/summaryLoyalty dashboard summary (auth required)
POST/merchants/:merchantId/loyalty/:programId/tiersCreate a tier (auth required)
GET/merchants/:merchantId/loyalty/:programId/tiersList tiers with member counts (auth required)
POST/merchants/:merchantId/loyalty/:programId/rewardsCreate a reward (auth required)
GET/merchants/:merchantId/loyalty/:programId/rewardsList rewards catalog (auth required)
POST/merchants/:merchantId/loyalty/:programId/redemptionsRedeem a reward for a member (auth required)
GET/merchants/:merchantId/loyalty/:programId/redemptionsList recent redemptions (auth required)

Affiliates & Referrals

GET/merchants/:merchantId/affiliatesAffiliate leaderboard for a merchant (auth required)
GET/merchants/:merchantId/referralsReferral funnel aggregates (auth required)
GET/merchants/:merchantId/affiliate-payablesPer-affiliate owed commission (auth required)
GET/merchants/:merchantId/affiliate-payoutsCommission payout history (auth required)
POST/merchants/:merchantId/affiliate-payoutsPay out all affiliates with a balance owed (auth required)
POST/merchants/:merchantId/affiliates/:affiliateId/payoutsPay out a single affiliate (auth required)
POST/affiliatesCreate an affiliate for the authenticated user (auth required)
POST/affiliates/:affiliateId/referralsTrack a referral (auth required)
Scope of the CRM API

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.

Message transport is a provider swap

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

POST/merchants/:merchantId/contactsCreate a contact
Request Body
{
"email": "[email protected]",
"name": "Jordan Lee",
"phone": "+15551234567",
"tags": ["vip", "newsletter"]
}
FieldTypeRequiredDescription
emailstringYesContact email
namestringNoContact name
phonestringNoContact phone number
tagsstring[]NoFree-form tags used for segmentation

GET /merchants/:merchantId/contacts returns all contacts for the merchant.

Campaigns

POST/merchants/:merchantId/campaignsCreate a marketing campaign
Request Body
{
"name": "Summer Sale",
"type": "email",
"subject": "20% off everything",
"body": "Our biggest sale of the year is here."
}
FieldTypeRequiredDescription
namestringYesCampaign name
typestringYesCampaign type, e.g. email, sms
subjectstringNoSubject line
bodystringNoCampaign body content

Send a Campaign

POST/merchants/:merchantId/campaigns/:id/sendSend/dispatch a campaign to its audience

Snapshots 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.

Request Body (optional)
{
"tag": "vip"
}
FieldTypeRequiredDescription
tagstringNoOnly queue contacts whose tags contain this value; omit to target all contacts
Response
{
"id": "cmp_abc123",
"status": "sent",
"sentCount": 128,
"recipientCount": 128,
"queued": 128
}

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.

POST/merchants/:merchantId/automationsCreate an automation
Request Body
{
"name": "Welcome series",
"trigger": "welcome",
"actionType": "email",
"status": "active",
"config": { "template": "welcome-1" }
}
FieldTypeRequiredDescription
namestringYesAutomation name
triggerstringYesOne of abandoned_cart, welcome, post_purchase, win_back
actionTypestringNoemail or sms (default: email)
statusstringNoactive, paused, or draft (default: active)
configobjectNoMessage template/settings payload

GET /merchants/:merchantId/automations lists automations newest-first, each with real runCount and lastRunAt counters (start at 0/null).

PATCH/merchants/:merchantId/automations/:idUpdate / pause / activate an automation

Accepts 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.

DELETE/merchants/:merchantId/automations/:idDelete an automation

Loyalty Programs

POST/merchants/:merchantId/loyaltyCreate a loyalty program
Request Body
{
"name": "Rewards Club",
"pointsPerDollar": 10
}
FieldTypeRequiredDescription
namestringYesProgram name
pointsPerDollarnumberNoPoints earned per dollar spent

GET /merchants/:merchantId/loyalty returns each program enriched with real member and point stats:

Response
[
{
  "id": "loy_abc123",
  "merchantId": "merch_abc123",
  "name": "Rewards Club",
  "pointsPerDollar": 10,
  "memberCount": 128,
  "totalPoints": 45200,
  "lifetimeEarned": 61000
}
]

Loyalty Summary

GET/merchants/:merchantId/loyalty/summaryLoyalty dashboard summary

Aggregates 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.

Response
{
"programs": 1,
"totalMembers": 128,
"pointsIssued": 61000,
"pointsRedeemed": 15800,
"pointsOutstanding": 45200,
"recentMembers": [
  { "programId": "loy_abc123", "userId": "usr_1", "balance": 320, "lifetimeEarned": 900, "updatedAt": "2026-06-09T12:00:00Z" }
],
"tiers": [
  { "id": "tier_1", "programId": "loy_abc123", "name": "Gold", "minPoints": 500, "sortOrder": 2, "memberCount": 12 }
],
"rewards": [
  { "id": "rwd_1", "programId": "loy_abc123", "name": "Free shipping", "pointsCost": 200, "active": 1 }
],
"recentRedemptions": [
  { "id": "rdm_1", "programId": "loy_abc123", "userId": "usr_1", "rewardId": "rwd_1", "pointsSpent": 200, "status": "pending", "rewardName": "Free shipping", "userEmail": "[email protected]" }
]
}

Tiers

POST/merchants/:merchantId/loyalty/:programId/tiersCreate a tier
Request Body
{
"name": "Gold",
"minPoints": 500,
"sortOrder": 2,
"perks": { "discount": "10%" }
}
FieldTypeRequiredDescription
namestringYesTier name
minPointsnumberNoMinimum point balance to reach this tier (default: 0)
sortOrdernumberNoDisplay order (default: 0)
perksobjectNoArbitrary 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

POST/merchants/:merchantId/loyalty/:programId/rewardsCreate a reward
Request Body
{
"name": "Free shipping",
"description": "One free shipment",
"pointsCost": 200,
"active": true
}
FieldTypeRequiredDescription
namestringYesReward name
descriptionstringNoReward description
pointsCostnumberYesPoint cost — must be a positive integer
activebooleanNoWhether the reward is redeemable (default: true)

GET .../rewards returns the catalog ordered by ascending pointsCost.

Redemptions

POST/merchants/:merchantId/loyalty/:programId/redemptionsRedeem a reward for a member
Request Body
{
"userId": "usr_1",
"rewardId": "rwd_1"
}
FieldTypeRequiredDescription
userIdstringYesMember redeeming the reward
rewardIdstringYesReward 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.

Response
{
"id": "rdm_1",
"programId": "loy_abc123",
"userId": "usr_1",
"rewardId": "rwd_1",
"pointsSpent": 200,
"status": "pending",
"balanceAfter": 120
}

GET .../redemptions returns recent redemptions joined to reward name and redeeming user.


Affiliates & Referrals

Create an Affiliate

POST/affiliatesCreate an affiliate for the authenticated user
Request Body
{
"code": "JORDAN10",
"commissionRate": 10
}
FieldTypeRequiredDescription
codestringYesUnique referral code
commissionRatenumberNoCommission percentage (default: 10)

Track a Referral

POST/affiliates/:affiliateId/referralsTrack a referral
Request Body
{
"referredUserId": "usr_2",
"commission": 500
}
FieldTypeRequiredDescription
referredUserIdstringYesThe referred user
commissionnumberYesCommission amount attributed to this referral

Affiliate Leaderboard

GET/merchants/:merchantId/affiliatesAffiliate leaderboard for a merchant

Returns each affiliate enriched with a real referral count and summed commission, sorted by total earned (highest first).

Referral Stats

GET/merchants/:merchantId/referralsReferral funnel aggregates
Response
{
"affiliates": 4,
"total": 37,
"byStatus": { "pending": 12, "confirmed": 25 },
"totalCommission": 18400
}

Commission 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.

GET/merchants/:merchantId/affiliate-payablesPer-affiliate owed commission
Response
{
"totalOwed": 30000,
"affiliates": [
  { "affiliateId": "af_1", "code": "JORDAN10", "status": "active", "commissionRate": 10, "earned": 50000, "totalPaid": 20000, "owed": 30000 }
]
}
POST/merchants/:merchantId/affiliates/:affiliateId/payoutsPay out a single affiliate

Settles one affiliate's owed balance in a transaction (row-locked). Returns 400 if nothing is owed.

Response
{
"id": "po_1",
"affiliateId": "af_1",
"amount": 30000,
"status": "paid",
"affiliateCode": "JORDAN10"
}
POST/merchants/:merchantId/affiliate-payoutsPay out all affiliates with a balance owed

Pays out every affiliate whose owed > 0. Returns 400 if none have a balance.

Response
{
"count": 3,
"totalPaid": 30000,
"payouts": [
  { "id": "po_1", "affiliateId": "af_1", "amount": 30000, "status": "paid" }
]
}
GET/merchants/:merchantId/affiliate-payoutsCommission payout history

Returns recent payouts (newest first) joined to the affiliate code.

Next Steps

  • Merchants -- Create the merchant that owns these resources
  • Support -- Tickets, knowledge base, and chat
  • Analytics -- Revenue, fees, and conversion metrics