T
TOMO
Developer Docs
BETA These docs are under partner review. Some features described are roadmap items, not yet shipped. Verify against your sandbox before relying on any contract.

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_CONFLICT and you must use the /adjust endpoint 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_id
  • request_id (matches TOMO's original dispatch)
  • closed_at (ISO timestamp)
  • amount_inr
  • status
  • TOMO's response (closed_intent_id + cpc_event_id from 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.