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.
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.
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"
]
}
}{
"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
}
}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"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"])$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
120 requests / minute per key
30 requests / minute per key
60 requests / minute per origin
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-KeyIdempotency-Key (optional, POST /leads)X-Request-Id (optional, echoed)X-CP-Sandbox-Scenario (test keys only)
Response headers
X-Request-IdX-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-ResetRetry-After (429)Idempotent-ReplayedX-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
| Method | Path | Purpose | Keys | Notes |
|---|---|---|---|---|
| POST | /partner/v1/leads | Submit one lead | live | test | publishable | 201 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/bulk | Submit many leads | live | 202 {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/:id | One lead | live | test | state, reason, held reason, college, timeline (per your feedback mode), payouts, conversions. Also GET /partner/v1/leads?external_ref=… |
| GET | /partner/v1/leads | List leads | live | test | Filters: state, college, from, to (YYYY-MM-DD, IST), q, external_ref. Pagination: limit (≤200) + before_id cursor. |
| PATCH | /partner/v1/leads/:id | Correct a lead under review | live | test | Fields: course, state, city, email. Scope is re-checked. |
| DELETE | /partner/v1/leads/:id | Withdraw a lead under review | live | test | State becomes withdrawn. |
| POST | /partner/v1/leads/:id/dispute | Dispute a rejection or a return | live | {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/scope | Your granted colleges | live | test | publishable | Codes, 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-card | Your rates | live | Per event: lead, application, admission. |
| GET | /partner/v1/payouts?month=YYYY-MM | Payout items and statements | live | Items 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/:id | One payout statement | live | Lines (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.csv | Statement lines as CSV | live | |
| GET | /partner/v1/statements/:token | Statement PDF by share link | none (7-day signed link from run.statement_ready) | |
| GET | /partner/v1/digest/:token | Daily digest CSV | none (48-hour signed link from the daily digest mail) | |
| POST | /partner/v1/college/events | College events (application, admission, return) | college HMAC signature (X-CP-College, X-CP-Timestamp, X-CP-Signature, Idempotency-Key) | |
| GET | /partner/v1/batches/:id | Bulk batch results | live | Counts + per-row results; /errors.csv for the rows that did not store. |
| GET | /partner/v1/webhooks/log | Recent webhook deliveries | live | test | |
| POST | /partner/v1/webhooks/replay/:id | Replay a delivery | live | test | At most 10 per hour. |
| POST | /partner/v1/caller/config | Caller link: page config | caller token (body.token) | |
| POST | /partner/v1/caller/leads | Caller link: one lead | caller token (body.token) | |
| POST | /partner/v1/caller/bulk | Caller link: many leads | caller token (body.token) | |
| GET | /partner/v1/health · /docs · /openapi.json · /changelog | Public | none |
“Try it” is intentionally absent: keys are server-side secrets. Download the OpenAPI document to generate a client.
Validation
mobile | 10 digits starting 6–9 (after stripping +91 / spaces) |
name | 2–160 characters: letters in any language, spaces, dots, apostrophes and hyphens |
state | an Indian state or UT; a near miss is answered with a suggestion (state_suggestion, or hint on state_not_allowed) |
email | optional, must be valid when present |
college | a code from GET /scope (required) |
course | must 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
| State | Meaning |
|---|---|
under_review | Received — under review. Every lead is checked by CollegePucho before it is sent to the college. |
hold | Needs correction before it can be approved. |
approved_waiting | Approved — awaiting the college. |
delivered | Delivered to the college. |
contacted | The student was contacted. |
callback | The student asked to be called back. |
interested | The student is interested. |
not_interested | The student said they are not interested. |
application | Application confirmed by the college. |
admission | Admission confirmed by the college. |
not_accepted | Not accepted by the college. |
returned | Returned. |
rejected | Rejected at review. |
duplicate | This student was already submitted by you for this college. Recorded, but not payable. |
withdrawn | Withdrawn by you. |
expired | Not 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
| Code | Text shown to you | Payout | Resubmit? |
|---|---|---|---|
rejected_duplicate | Rejected — this student is already in our system for this college. | none | no |
out_of_scope | Rejected — outside your granted scope. | none | no |
invalid_contact | Rejected — the contact details are not valid. | none | yes, after correction |
fake_or_test | Rejected — the lead looks like test or fake data. | none | no |
incomplete | Rejected — required details were missing and not corrected in time. | none | yes, after correction |
consent_missing | Rejected — no consent record. | none | yes, after correction |
college_unavailable | Rejected — the college is not accepting leads right now. | none | yes, after correction |
other | Rejected. | none | no |
Held (approved, waiting for the college)
| Code | Text |
|---|---|
capacity_day | The college is at capacity today; approved leads are delivered when capacity opens. CollegePucho's own leads are served first. |
capacity_month | The college is at capacity for this month; approved leads are delivered when capacity opens. |
internal_reserve | Waiting for capacity — CollegePucho's own leads are served first; partner leads follow later in the day. |
college_paused | The college is temporarily not accepting leads. |
not_accepting_partner_leads | The college is temporarily not accepting partner leads. |
our_setup | Being prepared on our side before delivery. |
no_integration | Being handed over to the college by our team. |
manual_handover | Approved — being handed over to the college by our team (this college has no direct connection). |
already_attempted | Approved — an earlier delivery attempt failed on our side; our team is fixing it and will send it again. |
delivery_failing | Delivery to the college is being retried. |
duplicate_review | Paused for a duplicate check — this student may already be with the college. CollegePucho reviews it before anything is sent. |
partner_paused | Delivery is paused while your account is paused. |
Returns by the college
| Code | Text | Payout |
|---|---|---|
wrong_number | Returned — wrong number. | reversed |
not_the_student | Returned — the person reached was not the student. | reversed |
unreachable | Returned — not reachable after repeated attempts. | reversed |
already_enrolled_before | Returned — the student had already enrolled before this lead. | reversed |
fake | Returned — the lead was found to be fake. | reversed |
no_consent | Returned — the student did not consent to be contacted. | reversed |
college_returned | Returned by the college. | reversed |
capacity_timeout | Returned — 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 }.
| Code | HTTP | Message | What to do |
|---|---|---|---|
invalid_name | 422 | A student name of at least 2 characters is required. | — |
invalid_mobile | 422 | Mobile must be a valid 10-digit Indian number (starting 6–9). | Resubmit with a corrected number. |
invalid_email | 422 | The email address is not valid. | Fix or omit the email. |
unknown_fields | 422 | The request has fields this API does not accept. | Remove them, or put custom values under "data" (up to 20 keys). |
consent_required | 422 | A consent record is required for every lead. | Send consent {version, at, channel, scope} with the lead. |
guardian_consent_required | 422 | A student under 18 needs a guardian's consent. | Add guardian {name, relation, at}. |
college_required | 422 | Name the college this lead is for. | Use a college code from your scope. |
college_not_granted | 403 | That college is not one of your granted colleges. | Check your scope for the colleges you may submit to. |
no_grants | 403 | No colleges are granted to your account yet. | Ask your account manager to add a college. |
grant_paused | 403 | Submissions for this college are paused. | Choose another granted college or wait for it to resume. |
course_not_allowed | 403 | That course is not allowed for this college. | Use one of the courses listed in your scope for the college. |
state_not_allowed | 403 | That state is outside the college's allowed states. | Correct and resubmit. |
source_locked | 403 | Source mismatch — send your assigned source or omit the field. | — |
lifecycle_paused | 403 | Your partner account is not live. Contact your account manager. | — |
key_expired | 401 | API key expired. Ask your account manager for a new key. | — |
key_revoked | 401 | This API key was revoked. | — |
unknown_key | 401 | Unknown API key. | — |
missing_key | 401 | Missing X-API-Key header. | — |
ip_blocked | 403 | This IP address is not on your whitelist. | — |
cap_day | 429 | Daily lead limit reached. It resets at midnight IST. | — |
cap_month | 429 | Monthly lead limit reached. It resets on the 1st (IST). | — |
cap_hour | 429 | Hourly lead limit reached. Try again later. | — |
rate_limited | 429 | Per-minute limit reached. Slow down and retry. | — |
cap_grant_day | 429 | Today's allowance for this college is used up. It resets at midnight IST. | — |
cap_grant_month | 429 | This month's allowance for this college is used up. It resets on the 1st (IST). | — |
link_invalid | 403 | This link is no longer valid. Ask your partner for a new link. | — |
link_expired | 403 | This link has expired. Ask your partner for a new link. | — |
link_revoked | 403 | This link was turned off by your partner. Ask them for a new link. | — |
not_enabled | 403 | This feature is not enabled for your account. Ask your account manager. | — |
duplicate_paid | 409 | A lead for this student and college was already paid this season. | — |
duplicate_unresolved | 409 | A possible duplicate must be resolved before approval. | — |
partner_target_locked | 403 | A partner lead goes only to the college it was submitted for. | — |
origin_not_allowed | 403 | This origin is not allowed for the publishable key. | Add the site's origin (https://your-site.com) to the key in the portal. |
forbidden_scope | 403 | This key may not perform that action. | Use a live key with the right scope. |
sandbox_only | 403 | Use this key with the Partner API v1 (/partner/v1); test keys write to the sandbox only. | — |
idempotency_mismatch | 409 | This Idempotency-Key was already used with a different request body. | — |
idempotency_in_progress | 409 | A request with this Idempotency-Key is still being processed. | Retry after a moment. |
not_found | 404 | Not found. | — |
not_editable | 409 | Only a lead under review can be corrected. | — |
not_withdrawable | 409 | Only a lead under review can be withdrawn. | — |
not_disputable | 409 | Only a rejected or returned lead can be disputed, within its window. | — |
batch_not_found | 404 | Batch not found. | — |
empty_batch | 422 | Send rows with at least one lead. | — |
batch_too_large | 422 | At most 2000 rows per batch. | — |
webhook_url_invalid | 422 | The webhook URL must be a public https address. | — |
replay_limit | 429 | Replay limit reached — at most 10 replays per hour. | — |
cap_caller_day | 429 | Your daily limit for this link is reached. It resets at midnight IST. | — |
Consent
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.
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));
});
}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(","))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.
| Scenario | Mobile | Journey |
|---|---|---|
approved_deliveredApproved and delivered | 9000000001 | 20s → approved_waiting → 20s → delivered |
rejected_duplicateRejected at review as a duplicate | 9000000002 | 20s → rejected (rejected_duplicate) |
rejected_out_of_scopeRejected as out of scope | 9000000003 | 20s → rejected (out_of_scope) |
held_capacityApproved, held for capacity, delivered later | 9000000004 | 20s → approved_waiting → 10s → approved_waiting (capacity_day) → 60s → delivered |
returnedDelivered, then returned (wrong number) | 9000000005 | 20s → approved_waiting → 10s → delivered → 60s → returned (wrong_number) |
applicationDelivered, then application confirmed | 9000000006 | 20s → approved_waiting → 10s → delivered → 60s → application |
admissionDelivered, application, admission | 9000000007 | 20s → approved_waiting → 10s → delivered → 40s → application → 40s → admission |
webhook_500Approved and delivered; the first 3 webhook attempts are recorded as HTTP 500 so you can watch our retries | 9000000008 | 15s → approved_waiting → 15s → delivered |
slowStays under review for 10 minutes, then approved | 9000000009 | 600s → 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
- 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. - 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). - Statement — generated on the 8th for the previous month; approved by the 10th; PDF and JSON with a sha256 hash (
GET /runs/:id). - Invoice — GST-registered partners raise a tax invoice for the statement amount (SAC 9983); unregistered partners do not add GST.
- TDS — deducted under section 194H on the taxable amount; 20% when no valid PAN is on file; Form 16A each quarter.
- Payment — within 30 days (45 for MSME) by bank transfer to the verified account; the UTR appears on the statement.
Changelog & deprecation
- 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).
- 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).
- 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.
- 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.