TOMO Completion Contract — The Closed-Intent POST
Audience: every TOMO partner. This is the single most important call you make to TOMO. Get it right and you get paid. Get it wrong and the closed intent doesn't enter the CPC ledger.
1. When to fire
POST to TOMO's CPC webhook immediately after the intent transitions to a terminal state, success or failure. Terminal states (per intent):
| Intent shape | "Fire CPC" trigger |
|---|---|
| Listing/booking (hotel, flight, package) | check-in confirmed OR booking-cancelled-by-user OR booking-cancelled-by-provider |
| Order delivery (food, grocery) | order-delivered OR order-failed-irrevocably |
| Mobility (ride, intercity, self-drive) | ride-completed (drop reached) OR ride-cancelled |
| Service (salon, gym, doctor) | service-rendered OR service-cancelled |
| Marketplace (used car, electronics) | sale-completed OR sale-cancelled-after-handshake |
| Logistics (parcel, move) | delivered OR failed-delivery-irrevocable |
Don't fire on transient states. Cooking, en-route, pending-confirmation, processing — these are not terminal.
2. The endpoint
POST https://www.automobnxt.com/api/v1/cpc/mcp_provider/<your_partner_id>
Headers:
Content-Type: application/json
X-TOMO-Timestamp: <unix epoch ms>
X-TOMO-Signature: sha256=<hex hmac>
Body: universal envelope (§3) + intent-specific fields (per intent §7).
Signing per WEBHOOK_SIGNING.md. Rejection on signature failure: 401 SIGNATURE_INVALID.
3. Universal envelope — every intent
These 9 fields are required on every CPC POST regardless of intent:
{
"intent": "<full_intent_id>",
"intent_version": "<semver>",
"external_id": "<your_internal_id>",
"amount_inr": <integer>,
"closed_at": "<ISO_DATETIME with timezone>",
"request_id": "<echo from search/dispatch call>",
"status": "<terminal status enum from intent §7>",
"currency": "INR",
"notes": "<string, may be empty>"
}
| Field | Constraint | Notes |
|---|---|---|
intent |
REQUIRED, full intent ID | e.g., food.order_delivery, travel.book_hotel |
intent_version |
REQUIRED, semver | matches the intent_version from the original dispatch payload |
external_id |
REQUIRED, unique per closed intent | YOUR system's id — booking_ref, order_ref, ride_ref. Idempotency key. |
amount_inr |
REQUIRED, INR_INTEGER, ≥ 0 | total final amount user paid (or would have paid before refund). 0 only for free-tier closes. |
closed_at |
REQUIRED, ISO_DATETIME with TZ | the moment the intent became terminal in your system |
request_id |
REQUIRED | the request_id TOMO sent in the original search/create call. Echo verbatim. |
status |
REQUIRED, intent-specific enum | NOT free text. See your intent's §7. |
currency |
REQUIRED, always "INR" |
locked v1 |
notes |
REQUIRED, may be empty | partner-side context. Kept in audit log. |
4. Intent-specific fields (additions to the envelope)
Each intent's spec §7 lists additional REQUIRED fields. Examples:
travel.book_hotel
{
// ...universal envelope...
"booking_ref": "BOOK-12345",
"merchant_id": "ChIJxxxxx",
"check_in": "2026-05-15",
"check_out": "2026-05-17",
"rooms": 1,
"guests": 2,
"fees_breakdown_total_inr": 1200,
"cancellation_until": "2026-05-13T18:00:00+05:30"
}
food.order_delivery
{
// ...universal envelope...
"order_ref": "SWIGGY-ORDER-XYZ123",
"merchant_id": "ChIJxxxxx",
"restaurant_id": "swiggy_rest_28342",
"delivered_at": "2026-05-09T20:42:00+05:30",
"delivery_eta_minutes_promised": 35,
"delivery_eta_minutes_actual": 34,
"items_count": 3,
"fees_breakdown_total_inr": 122,
"rider_rating_given": null,
"restaurant_rating_given": null
}
mobility.book_intracity_ride
{
// ...universal envelope...
"ride_ref": "UBER-RIDE-XYZ",
"started_at": "2026-05-09T08:21:00+05:30",
"completed_at": "2026-05-09T09:02:00+05:30",
"distance_traveled_km": 28.7,
"duration_minutes": 41,
"promised_eta_minutes": 47,
"actual_eta_minutes": 41,
"fare_breakdown_total_inr": 480,
"rider_tip_inr": 0,
"ratings_pending": true
}
The full additional-field list per intent is in §7 of every intent spec. Don't skip these. Missing intent-specific fields → entry rejected → no commission accrued.
5. Status enum (per intent — never free text)
Each intent's §7 specifies the legal status values. Examples:
| Intent | Allowed status values |
|---|---|
travel.book_hotel |
confirmed | cancelled_by_user | cancelled_by_provider | no_show | failed_payment |
food.order_delivery |
delivered | failed_delivery | cancelled_by_user | cancelled_by_restaurant | cancelled_no_rider |
mobility.book_intracity_ride |
completed | cancelled_by_user | cancelled_by_driver | failed | rerouted_with_extra_charge |
mobility.book_intercity_ride |
completed | cancelled_by_user | cancelled_by_driver | failed | rerouted_with_extra_charge | aborted_safety_concern |
If you send a status not in the enum, TOMO returns 400 INVALID_STATUS and the entry is not created.
6. The 10% commission rule
TOMO charges 10% on amount_inr for every successfully closed intent.
commission_inr = round(amount_inr * 0.10)
partner_keeps = amount_inr - commission_inr
The commission accrual is computed at TOMO's ledger write step. It's visible to you in the Tier 1 dashboard's "CPC ledger" view, line-by-line, with closed_intent_id reference.
No exceptions. Same rate for solo drivers, kirana stores, OYO chains, MakeMyTrip aggregators. There are no enterprise discounts, no volume tiers. Rate transparency is the brand promise.
What counts as amount_inr?
The total amount the user paid (or would have paid) for the closed intent. Includes:
- Base price
- Fees (delivery, service, platform, packaging, GST, etc.)
- Surge multipliers
- Tips (if pre-collected through the partner)
Excludes:
- Refund-only credits issued to user
- Promotional discounts the partner funded (those are baked into final price already)
If status is cancelled_by_user or cancelled_by_provider and no money exchanged hands → amount_inr: 0. TOMO logs the closed_intent but commission is 0.
If a cancellation charge was applied → amount_inr: <cancellation_charge_inr>. Commission is 10% of that.
7. Refunds and adjustments
If the close transitions later (refund issued, dispute resolved, re-charge), POST a separate adjustment event:
POST https://www.automobnxt.com/api/v1/cpc/mcp_provider/<your_partner_id>/adjust
Body:
{
"external_id": "<original_external_id>",
"adjustment_kind": "refund_partial" | "refund_full" | "rebill" | "dispute_resolved",
"adjustment_inr": <signed_integer>,
"adjusted_at": "ISO_DATETIME",
"reason": "<STRICT ENUM>",
"request_id": "<original_request_id>"
}
adjustment_inr is signed — negative for refunds, positive for re-bills.
TOMO recomputes the commission on the adjusted total. If the original commission was already settled to AUTOMOBNXT's account, TOMO carries the delta into the next settlement window.
8. Settlement (when do you actually receive money?)
Monthly settlement. Last day of each calendar month, TOMO computes:
your_payable = sum(amount_inr) - sum(commission_inr) - sum(refund_adjustments)
And initiates an NEFT/IMPS transfer to your registered bank account on the 5th of the following month.
For partners with average monthly volume > ₹50,00,000, weekly settlement is available on request.
The Tier 1 dashboard shows your real-time accrual + the next settlement date. No invoices to chase. No human-touch finance loop.
9. Idempotency
CPC webhooks are idempotent on external_id. Same external_id POSTed multiple times = single closed_intent row. TOMO returns the same closed_intent_id and cpc_event_id on duplicate POSTs.
Use this for safe retries:
on network failure: retry with same external_id
on 401 SIGNATURE_INVALID: fix signing, retry with same external_id
on 5xx: retry with exponential backoff up to 5 attempts
on 2xx: stop
If you POST a different amount_inr for the same external_id:
- Within 60 seconds of original: TOMO accepts the correction (race condition tolerance)
- After 60 seconds: TOMO returns
409 IDEMPOTENCY_CONFLICTand you must use the/adjustendpoint instead
10. Error responses TOMO returns
| HTTP | Code | Meaning | Your action |
|---|---|---|---|
| 201 | (created) | New closed_intent + cpc_event written | done |
| 200 | (ok, idempotent) | Duplicate external_id, returning existing | done |
| 400 | INVALID_REQUEST |
Malformed body, missing required field | fix + retry |
| 400 | INVALID_INTENT |
intent field doesn't exist in catalog |
check intent ID |
| 400 | INVALID_STATUS |
status not in §7 enum for this intent |
use enum value |
| 400 | AMOUNT_NEGATIVE |
amount_inr < 0 |
fix |
| 400 | MISSING_INTENT_FIELDS |
Intent-specific fields per §7 absent | add them |
| 401 | SIGNATURE_INVALID |
HMAC verification failed | check signing |
| 401 | TIMESTAMP_OUTSIDE_WINDOW |
timestamp drift > 5 min | sync clock |
| 404 | PARTNER_NOT_FOUND |
partner_id in URL doesn't exist | check ID |
| 409 | IDEMPOTENCY_CONFLICT |
Same external_id, different fields, > 60s delta | use /adjust |
| 429 | RATE_LIMITED |
Too many CPC posts/min | backoff |
| 500 | INTERNAL_ERROR |
TOMO-side failure | retry with backoff |
11. Partner-side audit log requirement
You MUST keep your own audit log of every CPC POST you've made. Include:
external_idrequest_id(matches TOMO's original dispatch)closed_at(ISO timestamp)amount_inrstatus- TOMO's response (
closed_intent_id+cpc_event_idfrom a 201/200 response)
Retention: 7 years (Indian tax law for B2B revenue records).
If TOMO ops needs to reconcile a disputed entry, you must produce the audit log within 48h. Failure → suspension.
12. Common mistakes
POSTing for transient states
"order-accepted" or "ride-en_route" are NOT terminal. Don't fire CPC on these. Only on terminal closure.
Echoing wrong request_id
The original dispatch (search_*, create_*) carried a request_id. That's what you echo back. NOT a fresh ID.
Free-text status
"Successfully delivered with minor delay" — wrong. Use delivered. The enum is the contract.
Forgetting intent-specific fields
Universal envelope alone is insufficient. Each intent's §7 adds REQUIRED fields. Read your intent.
Sending currency: "USD"
v1 is INR-only. The partner-side conversion (e.g., Booking.com USD prices) happens BEFORE the CPC POST.
Using floats for amount_inr
INR_INTEGER means whole rupees. 840.50 is wrong. Round to integer (banking rounding, half-up to nearest rupee).
Missing the notes field even when empty
If you don't have notes, send "notes": "". Don't omit the key.
13. Sample full POST (travel.book_hotel)
POST /api/v1/cpc/mcp_provider/booking_com_partner HTTP/1.1
Host: www.automobnxt.com
Content-Type: application/json
X-TOMO-Timestamp: 1715257923000
X-TOMO-Signature: sha256=ab92f4...
{
"intent": "travel.book_hotel",
"intent_version": "v1.0.0",
"external_id": "BOOKING-CONFIRMATION-12345",
"amount_inr": 8400,
"closed_at": "2026-05-09T14:35:00+05:30",
"request_id": "req_8f3k2m_2026-05-09T14:32:00Z",
"status": "confirmed",
"currency": "INR",
"notes": "",
"booking_ref": "BOOK-12345",
"merchant_id": "ChIJxxxxx",
"check_in": "2026-05-15",
"check_out": "2026-05-17",
"rooms": 1,
"guests": 2,
"fees_breakdown_total_inr": 1200,
"cancellation_until": "2026-05-13T18:00:00+05:30"
}
Response (201):
{
"ok": true,
"closed_intent_id": "ci_8f4k2m_2026_05_09",
"cpc_event_id": "cpc_8f4k2m_2026_05_09",
"amount_inr": 8400,
"commission_inr": 840,
"partner_payable_inr": 7560,
"settlement_window_end": "2026-05-31",
"settlement_payout_at": "2026-06-05"
}
14. References
- HMAC details:
WEBHOOK_SIGNING.md - Per-intent §7: every file under
docs/intents/ - Server-side ingest:
server/routes/cpc.ts - Server-side ledger:
server/services/cpcLedgerFirestore.ts
Built by AUTOMOBNXT · DPIIT Recognised Startup · 2026.