CollegePucho Partner APIvv1OpenAPI JSONChangelogPartner portal

Partner API

Submit student leads for the colleges granted to you and follow each lead to its outcome. Server-to-server, JSON over HTTPS, additive versioning: clients must ignore unknown fields.

Base URL https://collegepucho.onrender.com/partner/v1 · header X-API-Key · all times ISO-8601 with offset · amounts in INR.

Quickstart

Create a test key in the portal (Developers → Keys), send a lead to the sandbox, watch it move through its states, then read it back. Live keys are issued when CollegePucho approves go-live.

CURL
POST https://collegepucho.onrender.com/partner/v1/leads
X-API-Key: pk_live_…
Idempotency-Key: 7c1f…
Content-Type: application/json

{
  "name": "Riya Sharma",
  "mobile": "9811112222",
  "email": "riya@example.com",
  "college": "ims-ghaziabad",
  "course": "MBA",
  "state": "Delhi",
  "city": "New Delhi",
  "external_ref": "CRM-55821",
  "campaign": "oct-fair",
  "consent": {
    "version": "v1-en",
    "at": "2026-10-02T09:10:00+05:30",
    "channel": "web",
    "scope": [
      "counselling_calls",
      "share_with_college"
    ]
  }
}
RESPONSE · 201
{
  "id": 918223,
  "submission_id": 1,
  "external_ref": "CRM-55821",
  "state": "under_review",
  "review_due_at": "2026-10-02T13:10:00+05:30",
  "warnings": [],
  "quota": {
    "day_left": 39,
    "month_left": 1811
  }
}
NODE
const res = await fetch("https://collegepucho.onrender.com/partner/v1/leads", {
  method: "POST",
  headers: { "X-API-Key": process.env.CP_KEY, "Idempotency-Key": crypto.randomUUID(), "Content-Type": "application/json" },
  body: JSON.stringify(lead),
});
const data = await res.json(); // data.state === "under_review"
PYTHON
import os, uuid, requests
r = requests.post("https://collegepucho.onrender.com/partner/v1/leads",
    headers={"X-API-Key": os.environ["CP_KEY"], "Idempotency-Key": str(uuid.uuid4())},
    json=lead, timeout=10)
print(r.status_code, r.json()["state"])
PHP
$ch = curl_init("https://collegepucho.onrender.com/partner/v1/leads");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ["X-API-Key: " . getenv("CP_KEY"), "Idempotency-Key: " . bin2hex(random_bytes(16)), "Content-Type: application/json"],
  CURLOPT_POSTFIELDS => json_encode($lead)]);
$data = json_decode(curl_exec($ch), true); // $data["state"] === "under_review"

Authentication & keys

pk_live_…

production — writes real leads

pk_test_…

sandbox — scripted journeys, nothing reaches a college

pk_pub_…

publishable — browser forms on your allowed origins, POST /leads and GET /scope only

Rotating a key keeps the old one working for 24 hours; responses then carry X-CP-Key-Deprecated: true. Keys are shown once at creation and never stored in readable form. Live keys may be restricted to an IP allow-list (403 ip_blocked otherwise); test keys are not restricted.

The API is server-to-server. Browser requests with live or test keys are refused by CORS by design — use a publishable key, bound to your origins, for embedded forms (it can only POST /leads and GET /scope).

Rate limits & headers

LIVE

120 requests / minute per key

TEST

30 requests / minute per key

PUBLISHABLE

60 requests / minute per origin

QUOTAS

Your account also has per-minute, hourly, daily and monthly lead allowances and per-college daily caps; a 429 body carries the code and the reset time.

Request headers

  • X-API-Key
  • Idempotency-Key (optional, POST /leads)
  • X-Request-Id (optional, echoed)
  • X-CP-Sandbox-Scenario (test keys only)

Response headers

  • X-Request-Id
  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset
  • Retry-After (429)
  • Idempotent-Replayed
  • X-CP-Key-Deprecated

Idempotency & external_ref

Send Idempotency-Key with POST /leads: the same key with the same body returns the stored response (200, Idempotent-Replayed: true); the same key with a different body is 409 idempotency_mismatch. external_ref is independent: a repeated external_ref also replays.

Endpoints

MethodPathPurposeKeysNotes
POST/partner/v1/leadsSubmit one leadlive | test | publishable201 under_review (or 200 with Idempotent-Replayed: true). Required: name, mobile, college (code), consent (when your account requires it). Optional: email, course, state, city, external_ref, campaign, data{≤20 keys}, dob | is_minor, guardian. Header Idempotency-Key (optional).
POST/partner/v1/leads/bulkSubmit many leadslive202 {batch_id}. Up to 5000 rows (8 MB); stored set-based, typically within a minute; rows a quota refuses are deferred to the next window, never failed. Results at GET /partner/v1/batches/:id; batch.completed webhook.
GET/partner/v1/leads/:idOne leadlive | teststate, reason, held reason, college, timeline (per your feedback mode), payouts, conversions. Also GET /partner/v1/leads?external_ref=…
GET/partner/v1/leadsList leadslive | testFilters: state, college, from, to (YYYY-MM-DD, IST), q, external_ref. Pagination: limit (≤200) + before_id cursor.
PATCH/partner/v1/leads/:idCorrect a lead under reviewlive | testFields: course, state, city, email. Scope is re-checked.
DELETE/partner/v1/leads/:idWithdraw a lead under reviewlive | testState becomes withdrawn.
POST/partner/v1/leads/:id/disputeDispute a rejection or a returnlive{reason: fake|invalid|other, note, attachments: [≤3 {file_name, content_type, data_base64}]}. PDF/JPEG/PNG/WebP, 2 MB each. Opens a case our team answers in the portal.
GET/partner/v1/scopeYour granted collegeslive | test | publishableCodes, courses (with aliases), states, capacity state (open | tight | full), paused flag, exact remaining allowance per college and for the account, response mode, hold and return windows.
GET/partner/v1/rate-cardYour rateslivePer event: lead, application, admission.
GET/partner/v1/payouts?month=YYYY-MMPayout items and statementsliveItems carry state (accrued_on_hold → payable → in_run → paid; reversed / clawback_pending), the statement number once included, and the hold date.
GET/partner/v1/runs/:idOne payout statementliveLines (lead ids and your refs only), taxable, GST expected, TDS section/rate/amount, total payable, due date, payment; pdf_url valid 15 minutes.
GET/partner/v1/runs/:id/lines.csvStatement lines as CSVlive
GET/partner/v1/statements/:tokenStatement PDF by share linknone (7-day signed link from run.statement_ready)
GET/partner/v1/digest/:tokenDaily digest CSVnone (48-hour signed link from the daily digest mail)
POST/partner/v1/college/eventsCollege events (application, admission, return)college HMAC signature (X-CP-College, X-CP-Timestamp, X-CP-Signature, Idempotency-Key)
GET/partner/v1/batches/:idBulk batch resultsliveCounts + per-row results; /errors.csv for the rows that did not store.
GET/partner/v1/webhooks/logRecent webhook deliverieslive | test
POST/partner/v1/webhooks/replay/:idReplay a deliverylive | testAt most 10 per hour.
POST/partner/v1/caller/configCaller link: page configcaller token (body.token)
POST/partner/v1/caller/leadsCaller link: one leadcaller token (body.token)
POST/partner/v1/caller/bulkCaller link: many leadscaller token (body.token)
GET/partner/v1/health · /docs · /openapi.json · /changelogPublicnone

“Try it” is intentionally absent: keys are server-side secrets. Download the OpenAPI document to generate a client.

Validation

mobile10 digits starting 6–9 (after stripping +91 / spaces)
name2–160 characters: letters in any language, spaces, dots, apostrophes and hyphens
statean Indian state or UT; a near miss is answered with a suggestion (state_suggestion, or hint on state_not_allowed)
emailoptional, must be valid when present
collegea code from GET /scope (required)
coursemust be in the college's allowed list when the grant restricts courses
consent{version, at, channel, scope[], language?}; is_minor or dob under 18 needs guardian {name, relation}

States

StateMeaning
under_reviewReceived — under review. Every lead is checked by CollegePucho before it is sent to the college.
holdNeeds correction before it can be approved.
approved_waitingApproved — awaiting the college.
deliveredDelivered to the college.
contactedThe student was contacted.
callbackThe student asked to be called back.
interestedThe student is interested.
not_interestedThe student said they are not interested.
applicationApplication confirmed by the college.
admissionAdmission confirmed by the college.
not_acceptedNot accepted by the college.
returnedReturned.
rejectedRejected at review.
duplicateThis student was already submitted by you for this college. Recorded, but not payable.
withdrawnWithdrawn by you.
expiredNot reviewed in time (our fault). It is not counted against you.

Flow: under_review (or hold when a correction is needed) → approved_waiting → delivered → contacted / interested → application → admission. Terminal states: rejected, duplicate, withdrawn, expired, not_accepted, returned. What you see after delivery depends on your feedback mode (none, stages, full).

Reason codes

Rejection

CodeText shown to youPayoutResubmit?
rejected_duplicateRejected — this student is already in our system for this college.noneno
out_of_scopeRejected — outside your granted scope.noneno
invalid_contactRejected — the contact details are not valid.noneyes, after correction
fake_or_testRejected — the lead looks like test or fake data.noneno
incompleteRejected — required details were missing and not corrected in time.noneyes, after correction
consent_missingRejected — no consent record.noneyes, after correction
college_unavailableRejected — the college is not accepting leads right now.noneyes, after correction
otherRejected.noneno

Held (approved, waiting for the college)

CodeText
capacity_dayThe college is at capacity today; approved leads are delivered when capacity opens. CollegePucho's own leads are served first.
capacity_monthThe college is at capacity for this month; approved leads are delivered when capacity opens.
internal_reserveWaiting for capacity — CollegePucho's own leads are served first; partner leads follow later in the day.
college_pausedThe college is temporarily not accepting leads.
not_accepting_partner_leadsThe college is temporarily not accepting partner leads.
our_setupBeing prepared on our side before delivery.
no_integrationBeing handed over to the college by our team.
manual_handoverApproved — being handed over to the college by our team (this college has no direct connection).
already_attemptedApproved — an earlier delivery attempt failed on our side; our team is fixing it and will send it again.
delivery_failingDelivery to the college is being retried.
duplicate_reviewPaused for a duplicate check — this student may already be with the college. CollegePucho reviews it before anything is sent.
partner_pausedDelivery is paused while your account is paused.

Returns by the college

CodeTextPayout
wrong_numberReturned — wrong number.reversed
not_the_studentReturned — the person reached was not the student.reversed
unreachableReturned — not reachable after repeated attempts.reversed
already_enrolled_beforeReturned — the student had already enrolled before this lead.reversed
fakeReturned — the lead was found to be fake.reversed
no_consentReturned — the student did not consent to be contacted.reversed
college_returnedReturned by the college.reversed
capacity_timeoutReturned — the college could not take the lead within the holding period. You may submit it again later.none

Errors

Every error body is { code, message, hint?, errors?[] }; 422 bodies list field errors as { field, code, message }.

CodeHTTPMessageWhat to do
invalid_name422A student name of at least 2 characters is required.—
invalid_mobile422Mobile must be a valid 10-digit Indian number (starting 6–9).Resubmit with a corrected number.
invalid_email422The email address is not valid.Fix or omit the email.
unknown_fields422The request has fields this API does not accept.Remove them, or put custom values under "data" (up to 20 keys).
consent_required422A consent record is required for every lead.Send consent {version, at, channel, scope} with the lead.
guardian_consent_required422A student under 18 needs a guardian's consent.Add guardian {name, relation, at}.
college_required422Name the college this lead is for.Use a college code from your scope.
college_not_granted403That college is not one of your granted colleges.Check your scope for the colleges you may submit to.
no_grants403No colleges are granted to your account yet.Ask your account manager to add a college.
grant_paused403Submissions for this college are paused.Choose another granted college or wait for it to resume.
course_not_allowed403That course is not allowed for this college.Use one of the courses listed in your scope for the college.
state_not_allowed403That state is outside the college's allowed states.Correct and resubmit.
source_locked403Source mismatch — send your assigned source or omit the field.—
lifecycle_paused403Your partner account is not live. Contact your account manager.—
key_expired401API key expired. Ask your account manager for a new key.—
key_revoked401This API key was revoked.—
unknown_key401Unknown API key.—
missing_key401Missing X-API-Key header.—
ip_blocked403This IP address is not on your whitelist.—
cap_day429Daily lead limit reached. It resets at midnight IST.—
cap_month429Monthly lead limit reached. It resets on the 1st (IST).—
cap_hour429Hourly lead limit reached. Try again later.—
rate_limited429Per-minute limit reached. Slow down and retry.—
cap_grant_day429Today's allowance for this college is used up. It resets at midnight IST.—
cap_grant_month429This month's allowance for this college is used up. It resets on the 1st (IST).—
link_invalid403This link is no longer valid. Ask your partner for a new link.—
link_expired403This link has expired. Ask your partner for a new link.—
link_revoked403This link was turned off by your partner. Ask them for a new link.—
not_enabled403This feature is not enabled for your account. Ask your account manager.—
duplicate_paid409A lead for this student and college was already paid this season.—
duplicate_unresolved409A possible duplicate must be resolved before approval.—
partner_target_locked403A partner lead goes only to the college it was submitted for.—
origin_not_allowed403This origin is not allowed for the publishable key.Add the site's origin (https://your-site.com) to the key in the portal.
forbidden_scope403This key may not perform that action.Use a live key with the right scope.
sandbox_only403Use this key with the Partner API v1 (/partner/v1); test keys write to the sandbox only.—
idempotency_mismatch409This Idempotency-Key was already used with a different request body.—
idempotency_in_progress409A request with this Idempotency-Key is still being processed.Retry after a moment.
not_found404Not found.—
not_editable409Only a lead under review can be corrected.—
not_withdrawable409Only a lead under review can be withdrawn.—
not_disputable409Only a rejected or returned lead can be disputed, within its window.—
batch_not_found404Batch not found.—
empty_batch422Send rows with at least one lead.—
batch_too_large422At most 2000 rows per batch.—
webhook_url_invalid422The webhook URL must be a public https address.—
replay_limit429Replay limit reached — at most 10 replays per hour.—
cap_caller_day429Your daily limit for this link is reached. It resets at midnight IST.—

Every lead carries the student's consent: consent: { version, at, channel, scope[], language? }. scope normally holds contact and share_with_college. A student under 18 (is_minor or dob) needs guardian: { name, relation }. Keep your own record of the notice text shown for each version; the notice is available in English and Hindi.

Webhooks

Configure a URL in the portal (Developers → Webhook). https only; a public hostname (no IPs, no private networks); no redirects; the endpoint must answer 2xx to a webhook.test ping when saved.

Events

lead.receivedlead.approvedlead.rejectedlead.holdlead.withdrawnlead.expiredlead.editedlead.deliveredlead.heldlead.not_acceptedlead.stagelead.returnedlead.supersededlead.erasedlead.consent_withdrawndispute.openeddispute.updateddispute.resolvedconversion.applicationconversion.admissionpayout.accruedpayout.payablepayout.reversedpayout.clawbackrun.statement_readypayout.paidbatch.completedscope.changedgrant.pausedgrant.resumedrate_card.changedkey.expiringkey.rotatedpartner.lifecyclewebhook.disabledwebhook.test

Headers

  • X-CP-Event
  • X-CP-Delivery (uuid — dedupe on it)
  • X-CP-Timestamp (unix seconds)
  • X-CP-Signature: v1=HMAC-SHA256(secret, timestamp + "." + rawBody) — may list two signatures while a rotated secret overlaps
  • X-CP-Env (live | test)

Payload

{
  "schema_version": 1,
  "event": "lead.delivered",
  "env": "live",
  "occurred_at": "2026-10-03T09:10:00.000Z",
  "data": {
    "…": "event-specific ids, refs, codes, amounts — never student details"
  },
  "submission": {
    "id": 918223,
    "submission_id": 1,
    "external_ref": "CRM-55821",
    "state": "delivered",
    "reason_code": null,
    "held_reason": null,
    "college_code": "ims-ghaziabad"
  }
}

Verify the signature

Recompute HMAC-SHA256 over `${X-CP-Timestamp}.${raw request body}` with your signing secret, compare constant-time to any listed v1 value, and reject timestamps older than 300 seconds.

NODE
const crypto = require("crypto");
function verify(rawBody, headers, secret) {
  const ts = headers["x-cp-timestamp"];
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(ts + "." + rawBody).digest("hex");
  return String(headers["x-cp-signature"]).split(",").some((s) => {
    const v = s.trim().replace(/^v1=/, "");
    return v.length === expected.length && crypto.timingSafeEqual(Buffer.from(v), Buffer.from(expected));
  });
}
PYTHON
import hmac, hashlib, time
def verify(raw_body: bytes, headers, secret: str) -> bool:
    ts = headers["X-CP-Timestamp"]
    if abs(time.time() - int(ts)) > 300: return False
    expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(s.strip().removeprefix("v1="), expected) for s in headers["X-CP-Signature"].split(","))
PHP
function verify(string $rawBody, array $headers, string $secret): bool {
  $ts = $headers["X-CP-Timestamp"];
  if (abs(time() - (int)$ts) > 300) return false;
  $expected = hash_hmac("sha256", $ts . "." . $rawBody, $secret);
  foreach (explode(",", $headers["X-CP-Signature"]) as $s) if (hash_equals($expected, preg_replace("/^v1=/", "", trim($s)))) return true;
  return false;
}

Retries, ordering, replay

Non-2xx or no answer within 8 s → retried after 1 min, 5 min, 30 min, 2 h and 12 h, then marked failed. 25 consecutive failures disable the endpoint (owners are emailed; re-enable from the portal). Order is not guaranteed; dedupe on X-CP-Delivery. Past deliveries can be replayed from the portal (10 per hour).

Sandbox

Use a pk_test_ key. Leads go to a sandbox book with a scripted journey chosen by the mobile number below or the X-CP-Sandbox-Scenario header; webhooks fire with env test.

ScenarioMobileJourney
approved_delivered
Approved and delivered
900000000120s → approved_waiting → 20s → delivered
rejected_duplicate
Rejected at review as a duplicate
900000000220s → rejected (rejected_duplicate)
rejected_out_of_scope
Rejected as out of scope
900000000320s → rejected (out_of_scope)
held_capacity
Approved, held for capacity, delivered later
900000000420s → approved_waiting → 10s → approved_waiting (capacity_day) → 60s → delivered
returned
Delivered, then returned (wrong number)
900000000520s → approved_waiting → 10s → delivered → 60s → returned (wrong_number)
application
Delivered, then application confirmed
900000000620s → approved_waiting → 10s → delivered → 60s → application
admission
Delivered, application, admission
900000000720s → approved_waiting → 10s → delivered → 40s → application → 40s → admission
webhook_500
Approved and delivered; the first 3 webhook attempts are recorded as HTTP 500 so you can watch our retries
900000000815s → approved_waiting → 15s → delivered
slow
Stays under review for 10 minutes, then approved
9000000009600s → approved_waiting → 30s → delivered

Scope & capacity

GET /scope lists the colleges granted to you with their codes, allowed courses and states, and a cap_state of open, tight or full. Approved leads are delivered when the college has capacity; CollegePucho's own leads are served first and a per-college reserve is kept for partner leads. A held lead shows a held reason (for example capacity_day) until it is delivered, returned or expires after the hold window.

How you get paid

  1. Rate card — per college, course and state, for the events lead, application and admission (GET /rate-card). Decreases are announced at least 7 days ahead.
  2. Items — a lead payout accrues on delivery and is on hold until the return window closes; application and admission payouts become payable when the college confirms (GET /payouts).
  3. Statement — generated on the 8th for the previous month; approved by the 10th; PDF and JSON with a sha256 hash (GET /runs/:id).
  4. Invoice — GST-registered partners raise a tax invoice for the statement amount (SAC 9983); unregistered partners do not add GST.
  5. TDS — deducted under section 194H on the taxable amount; 20% when no valid PAN is on file; Form 16A each quarter.
  6. Payment — within 30 days (45 for MSME) by bank transfer to the verified account; the UTR appears on the statement.
Why 2%?Commission income falls under section 194H of the Income-tax Act.
Why 20%?The Act requires the higher rate when no valid, operative PAN is on file. Add your PAN in the portal.
I am unregistered — do I add GST?No. Your statement carries no GST and you do not raise a tax invoice.
What is a duplicate?The same mobile already submitted for the same college within 30 days, or already with that college from another source. Recorded, not payable.

Changelog & deprecation

v1.3.0 · 2026-10-03
  • 429 answers carry code rate_limited, retry_after_seconds and reset_at with Retry-After; X-RateLimit-* headers show the limit that applies to the key (live 120, test 30, publishable 60 per website).
  • Every /partner/v1 response (public, caller and college routes too) carries X-Request-Id; error bodies include request_id.
  • GET /scope: exact remaining allowance per college and for the account, and course aliases.
  • Names must be letters (any language); a misspelt state gets a suggestion.
  • Disputes take up to 3 attachments (PDF/JPEG/PNG/WebP, 2 MB each).
  • lead.edited carries changes {field: {from, to}}; a course change re-checks duplicates.
  • New webhook events fire: grant.paused, grant.resumed, dispute.updated (our reply on a case).
  • IP allowlists accept IPv6 and IPv6 CIDR; a refused request names your address (your_ip) and the portal shows the last refused address.
  • OpenAPI now covers every route, including runs, CSV exports, caller links, college events and signed links.
  • Referral links (/r/<code> on the website, with QR) and WhatsApp links: student-filled leads arrive through the same review as your other channels (channel referral / whatsapp).
v1.2.0 · 2026-10-03
  • Retired the v0 door: POST /lead-center/ingest and the /lead-center/entry* caller routes are gone — use POST /partner/v1/leads and /partner/v1/caller/*.
  • The single pre-v1 account key is no longer accepted; only pk_live_ / pk_test_ / pk_pub_ keys issued from the portal work.
  • Partner notifications: lead holds, rejections, statements, payments, disputes, key and webhook events appear in the portal notification centre and by email per user preference.
  • Public partnership applications: /partner-program on the website (status applied).
v1.1.0 · 2026-10-03
  • Billing: GET /payouts returns payout items (on hold / payable / in statement / paid) and statements; GET /runs/:id returns one statement with a 15-minute PDF link; statement share links (7 days) at /partner/v1/statements/:token.
  • GET /rate-card now returns your rate card rows (college / course / state specific rows win).
  • Webhooks: payout.accrued, payout.payable, payout.reversed, payout.clawback, run.statement_ready, payout.paid, rate_card.changed, dispute.resolved now fire.
v1.0.0 · 2026-10-03
  • Partner API v1: leads (create, bulk, read, list, correct, withdraw, dispute), scope, rate card, payouts, batches, webhook log and replay.
  • Keys: pk_live_ / pk_test_ / pk_pub_ with 24-hour rotation overlap.
  • Sandbox: test keys write to a separate table with scripted scenarios.
  • Signed webhooks (X-CP-Signature v1) with retries and replay.

Versioning is additive: new fields and events may appear at any time; removals are announced here at least 90 days ahead and signalled with X-CP-Deprecated-Endpoint or X-CP-Key-Deprecated headers. Status page: status.collegepucho.com.