Custom CRM
REST contract
Implement the six fixed operations on your customer-owned HTTPS bridge. Pongg appends each POST path to the configured base URL.
Contract at a glance
Every request includes tenant_id and contract_version:pongg.crm.v1. Enforce the binding between that tenant and the presented credential. Return JSON objects. Contact upsert is required, together with at least one of leads.create or appointments.create.
Download the complete OpenAPI 3.1 document; it contains required fields, optional fields and synthetic examples.
curl --request POST "$BRIDGE_BASE_URL/capabilities" \
--header "Authorization: Bearer $BRIDGE_API_KEY" \
--header 'Content-Type: application/json' \
--data '{"tenant_id":"YOUR_PONGG_TENANT_ID","contract_version":"pongg.crm.v1"}'REST operations
Every operation is an HTTPS POST. Pongg appends the path to your configured base URL. For a base of https://crm.example.com/pongg/v1, capabilities is https://crm.example.com/pongg/v1/capabilities. All examples below are synthetic.
Every body contains tenant_id and contract_version. All three write operations also carry the same opaque 64-character hexadecimal key in the Idempotency-Key header and idempotency_key body field. Reads do not carry a write key.
POST /capabilities
Return the exact contract version and only the supported operation names. Include contacts.upsert and at least one of leads.create or appointments.create. If you advertise availability.list, also implement appointments.create and return booking_closes_availability:true for the same calendar.
Request JSON
{
"tenant_id": "tenant-example-001",
"contract_version": "pongg.crm.v1"
}Response JSON
{
"contract_version": "pongg.crm.v1",
"operations": [
"contacts.search",
"contacts.upsert",
"leads.create",
"availability.list",
"appointments.create"
],
"booking_closes_availability": true
}POST /contacts/search
Both search fields are strings and may be empty. Return contact:null when there is no match, or a nonblank durable contact ID. Do not invent a match.
Request JSON
{
"tenant_id": "tenant-example-001",
"contract_version": "pongg.crm.v1",
"phone": "+12145550148",
"email": "jordan@example.com"
}Response JSON
{
"contact": {
"id": "contact-example-001"
}
}{
"contact": null
}POST /contacts/upsert
Match phone or email identity before creating a contact. Return a durable nonblank ID and a real boolean is_new. String values such as "false" do not satisfy the response.
Request JSON
{
"tenant_id": "tenant-example-001",
"contract_version": "pongg.crm.v1",
"contact": {
"full_name": "Jordan Example",
"first_name": "Jordan",
"last_name": "Example",
"phones": [
"+12145550148"
],
"emails": [
"jordan@example.com"
],
"address": "100 Example St, Dallas TX 75201",
"source": "Pongg (phone)",
"tags": [
"Pongg"
],
"call_id": "example-call-001",
"postal_code": "75201"
},
"idempotency_key": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}Response JSON
{
"id": "contact-example-001",
"is_new": false
}POST /availability
The window includes the requested interval, IANA zone, weekly windows, duration and job type. Weekly windows are [day, openingMinute, closingMinute], with 0=Monday; an empty array means not supplied. slot_minutes can be 0 when unspecified. Return at most 50 real openings inside the interval, with end after start. Pongg sorts and deduplicates slots by instant. Service IDs or arrival-window metadata in a slot response are not consumed by this bridge.
Request JSON
{
"tenant_id": "tenant-example-001",
"contract_version": "pongg.crm.v1",
"window": {
"start": "2026-10-20T00:00:00+00:00",
"end": "2026-10-21T00:00:00+00:00",
"timezone": "America/Chicago",
"windows": [
[
1,
420,
1080
]
],
"slot_minutes": 60,
"job_type": "Roof repair"
}
}Response JSON
{
"slots": [
{
"start": "2026-10-20T15:00:00+00:00",
"end": "2026-10-20T16:00:00+00:00",
"timezone": "America/Chicago"
}
]
}POST /appointments
Check capacity and create the appointment atomically. Return a durable ID, confirmed:true, exact requested start/end instants and a valid IANA timezone. An unconfirmed request, shifted time, empty ID or missing timestamp is a failed booking. Return HTTP 409 if the slot is taken.
Request JSON
{
"tenant_id": "tenant-example-001",
"contract_version": "pongg.crm.v1",
"appointment": {
"contact_external_id": "contact-example-001",
"timezone": "America/Chicago",
"start": "2026-10-20T15:00:00+00:00",
"end": "2026-10-20T16:00:00+00:00",
"slot_text": "Tuesday at 10 AM Central",
"job_type": "Roof repair",
"address": "100 Example St, Dallas TX 75201",
"notes": "Synthetic example: inspect a small roof leak.",
"title": "Roof repair visit",
"source": "Pongg (phone)",
"location": {
"id": "dallas",
"name": "Dallas office"
},
"service_id": "repair-inspection"
},
"idempotency_key": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}Response JSON
{
"id": "appointment-example-001",
"confirmed": true,
"start": "2026-10-20T15:00:00+00:00",
"end": "2026-10-20T16:00:00+00:00",
"timezone": "America/Chicago"
}POST /leads
Create or return the existing lead for the same idempotency key. Return its durable nonblank ID. A generic ok:true acknowledgement is not a receipt.
Request JSON
{
"tenant_id": "tenant-example-001",
"contract_version": "pongg.crm.v1",
"lead": {
"contact": {
"full_name": "Jordan Example",
"first_name": "Jordan",
"last_name": "Example",
"phones": [
"+12145550148"
],
"emails": [
"jordan@example.com"
],
"address": "100 Example St, Dallas TX 75201",
"source": "Pongg (phone)",
"tags": [
"Pongg"
],
"call_id": "example-call-001",
"postal_code": "75201"
},
"job_type": "Roof repair",
"summary": "Caller requested an inspection.",
"notes": "Synthetic example only.",
"urgency": "routine",
"source": "Pongg (phone)",
"call_id": "example-call-001",
"location": {
"id": "dallas",
"name": "Dallas office"
}
},
"idempotency_key": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}Response JSON
{
"id": "lead-example-001"
}Receipts, time and retries
Use explicit UTC offsets in every timestamp and an IANA timezone such as America/Chicago. Appointment input is checked before sending; a naive or nonpositive interval is refused. A receipt must match both requested instants exactly. A prior availability read does not reserve capacity.
Persist a unique idempotency-key-to-receipt mapping in the same CRM transaction as each write. Replaying a write must return the original record. Contact keys are based on normalized identity; appointment keys use tenant, contact, UTC interval, service or job type and location; lead keys use call ID and job type when available. Mutable descriptions do not change those appointment or call-based lead keys. Treat keys as opaque.
| Result | What Pongg does |
|---|---|
2xx | Parses the required JSON object or MCP structured receipt. An empty response or acknowledgement alone is not success. |
409 | Treats the operation as a conflict or unavailable slot. No booking is confirmed and it is not automatically retried. |
429 or 5xx | Post-call delivery can retry under the worker policy with the same idempotency key. Retry-After is bounded to five minutes. |
| Network failure | Can retry in post-call delivery. A call-time operation returns a structured failure or fallback rather than promising an unconfirmed booking. |
401 | OAuth service accounts receive one fresh grant/replay. Further 401s, and API-key 401s, require corrected access. |
Other 4xx | Requires corrected configuration or data. Remote error bodies are not displayed verbatim or treated as successful receipts. |
Only the capabilities probe is part of connecting. The bridge must actually honor identity matching, idempotency, permissions and atomic slot closure. Test those behaviors with your team before relying on live calls.
Security and limits
- Use public HTTPS with a verifiable certificate. Every DNS answer must be public; the selected address is pinned while TLS verifies the original hostname.
- Redirects, proxies, URL credentials, query strings and fragments are not supported. Use a clean endpoint path.
- Each HTTP exchange has a 10-second total deadline. Request and response bodies are each limited to 1 MiB. Return uncompressed JSON or uncompressed MCP SSE.
- Bind each credential to its tenant. Reject a tenant mismatch even if the incoming phone number or CRM record looks valid.
- Store and rotate credentials securely. Pongg uses its tenant-bound encrypted integration store; never paste production secrets into examples, URLs or support messages.
- Keep caller data limited to the workflow your company authorized. Free-form notes and summaries need the same access controls as CRM records.
Failure responses
| Response | Pongg behavior | Bridge responsibility |
|---|---|---|
| 409 conflict | Permanent failure for this operation; no appointment is confirmed | Use for a taken slot. Do not return a successful receipt for a different time. |
| 401 | Service-account OAuth refreshes the grant and retries the same operation once; a second rejection stops | Return the same write receipt if the first request already committed. |
| Other 4xx / redirects | Permanent configuration or data failure | Correct the input or configuration. Do not depend on redirect following. |
| 429 / 5xx / network timeout | Transient failure; post-call worker retries under its policy | Honor idempotency even when a response was lost after the write committed. |
| Invalid receipt / malformed JSON | Rejected; not a confirmed write | Return the complete object documented for the operation. |
Do not rely on a particular remote error-body schema: Pongg surfaces a controlled error, not your raw body. In-call operations are time-bounded and do not wait through a post-call worker retry schedule. A successful capabilities probe never proves the write path.
