C2C Public API (v1)

C2C Public API (v1)

Overview

The C2C (tenant public) API is a versioned, JSON API scoped per tenant. All routes under /api/c2c share the same authentication, API version validation, and request logging.

Base URL pattern:

https://{your-tenant-host}/api/c2c

Example: https://mytenant.click2close.io/api/c2c

Full endpoint URLs are base URL + path, e.g. POST https://{tenant-host}/api/c2c/contacts/assets.


Required headers (all C2C endpoints)

Header

Required

Description

Header

Required

Description

X-Api-Version

Yes

Must be v1 (case-insensitive). Missing or empty → 400 missing_api_version. Unsupported version → 400 unsupported_api_version.

API key

Yes

See Authentication. Missing or invalid → 401 invalid_api_key.

Content-Type

Yes (for JSON bodies)

Must be application/json for endpoints that accept a body.


Authentication

Provide exactly one of:

  1. Authorization: Bearer c2c_{secret} — full key including the c2c_ prefix.

  2. X-Api-Key: c2c_{secret} — same string as the Bearer token.

API keys are tenant-scoped and issued in the product. The plaintext key must start with c2c_.

Available endpoints

Method

Path

Description

Method

Path

Description

GET

/api/c2c/health

Health check for the C2C stack on this tenant.

POST

/api/c2c/contacts/assets

Attach one or more base64-encoded files to an existing contact (identified by email).

POST

/api/c2c/coaching/sessions

Report aggregate coaching session progress for an existing client (identified by email)


GET /api/c2c/health

Success: HTTP 200

{ "data": { "status": "ok" } }

Use this to verify DNS, TLS, X-Api-Version, and API key before heavier integrations.


POST /api/c2c/contacts/assets

Uploads files as base64, decodes them on the server, and stores them as ** Media Library** assets on the Contact.

Purpose

  • Target contact must already exist (matched by email).

  • Attach 1–10 files per request; each file must pass MIME allowlist, size limits, and content-vs-declared-MIME checks.

Request

Headers

  • X-Api-Version: v1

  • Content-Type: application/json

  • Authorization: Bearer c2c_… or X-Api-Key: c2c_…

Body (JSON)

Field

Type

Required

Description

Field

Type

Required

Description

email

string

Yes

Contact email. Lookup is case-insensitive (trim + lowercase comparison). Contact must exist and not be soft-deleted.

files

array

Yes

Min 1, max 10 objects.

Each element of files

Field

Type

Required

Description

Field

Type

Required

Description

filename

string

Yes

Max 255 characters. Logical file name (basename used if a path is given).

mime_type

string

Yes

Must be one of the allowed MIME types listed below. Compared to detected MIME from decoded bytes (finfo); mismatches fail unless an allowed alias applies.

content

string

Yes

File bytes base64-encoded. Optional data URI prefix supported: data:...;base64,<payload> — only the segment after the last comma is decoded.

alt

string

No

Max 255. Optional accessibility text stored on the media record.

description

string

No

Max 1000. Optional description stored on the media record.

Allowed mime_type values

  • Images: image/jpeg, image/png, image/webp, image/gif, image/svg+xml

  • PDF: application/pdf

  • Spreadsheets / CSV: text/csv, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet

  • Plain text: text/plain

Size limits

Limit

Value

Limit

Value

Per file (decoded binary)

≤ 25 MB

Total across all files in one request

≤ 50 MB

MIME aliases

The server may accept a declared mime_type if detected MIME matches known aliases (for example: XLS/XLSX sometimes detected as application/octet-stream; CSV as text/plain). Exact equality between declared and detected MIME also passes.

Processing flow

  1. Validate JSON and custom rules (base64, sizes, MIME allowlist, MIME vs content).

  2. Resolve Contact with LOWER(email) = lower(trim(email)). If none → 422 contact_not_found.

  3. For each file in array order:

    • Decode base64 → write temp file.

    • Raster images (not SVG) whose original extension is not gif are converted to WebP before save (aligned with internal media upload behavior). GIF and SVG are not converted through that path.

    • Attach media to contact default collection, public on tenant disk.

  4. Return 201 with contact + created asset metadata.

  5. Unexpected server errors during upload → 500 store_failed (logged server-side).

Success response

HTTP 201 Created

{ "data": { "contact": { "uuid": "<contact_uuid>", "email": "<email_as_stored>" }, "assets": [ { "uuid": "<media_uuid>", "name": "<stored_name>", "mime_type": "<mime_type>", "type": "image|file", "size": 12345, "created_at": "YYYY-mm-dd HH:ii:ss" } ] } }
  • type: image if stored MIME is under image/*, otherwise file.

  • name: May differ from request filename (e.g. WebP conversion changes extension for eligible images).

Error responses

Standard envelope:

{ "error": "<code>", "message": "<message>" }

Validation (422):

{ "error": "validation_failed", "message": "<message>", "errors": {} }

HTTP

error

Typical cause

HTTP

error

Typical cause

400

missing_api_version

X-Api-Version missing or empty

400

unsupported_api_version

Version other than v1

401

invalid_api_key

Missing/invalid key or wrong prefix

422

validation_failed

Schema, base64, size, MIME allowlist, or MIME/content mismatch

422

contact_not_found

No contact for given email

500

store_failed

Unexpected error while storing media

User-visible message values may be localized (en / es) depending on server configuration (see resources/lang/*/api_error.php, key c2c_contact_asset).

Example (curl)

curl --request POST \ --url 'https://{tenant-host}/api/c2c/contacts/assets' \ --header 'Authorization: Bearer c2c_YOUR_KEY' \ --header 'Content-Type: application/json' \ --header 'X-Api-Version: v1' \ --data '{ "email": "user@example.com", "files": [ { "filename": "document.pdf", "mime_type": "application/pdf", "content": "<base64_here>" } ] }'

 

POST /api/c2c/coaching/sessions

Records the session counters an external coaching platform tracks for a client, and stores them against that client's coaching order.

Purpose

Target contact must already exist (matched by email) and must have at least one coaching order.

A coaching package is a product of type digital under the health-coaching shop category. Products of any other type or category are not coaching packages.

The payload is an aggregate per client, not a list of individual sessions. One rollup is stored per coaching order and is overwritten on every call.

Request

Headers

Header

Value

Header

Value

X-Api-Version

v1

Content-Type

application/json

Authorization

Bearer c2c_… (or X-Api-Key: c2c_…)

Body (JSON)

Field

Type

Required

Description

Field

Type

Required

Description

client_email

string

Yes

Contact email. Lookup is case-insensitive (trim + lowercase comparison). Contact must exist and not be soft-deleted.

sessions_purchased

integer

Yes

Minimum 0. Must equal the sessions included in the purchased package when a quota is configured — see Session quota. Otherwise → 422 sessions_mismatch.

sessions_completed

integer

Yes

Minimum 0. Must be less than or equal to sessions_purchased.

sessions_remaining

integer

No

Informational. The server always recomputes sessions_purchased − sessions_completed; a value that disagrees is recorded as a discrepancy and does not fail the request.

last_session_date

string

No

Date of the most recent completed session, format YYYY-MM-DD.

Session quota

How many sessions each coaching package includes is configured per package inside the product, in the Coaching dashboard under Session packages (for example 90 Day → 6, 6-Month → 12, 12-Month → 24).

The expected quota for a request is the sum of sessions_included × quantity across every coaching item on the matched order. The product is the authority: a sessions_purchased that disagrees is rejected rather than silently overwriting the configured value.

If no coaching item on the matched order has a quota configured yet, the check is skipped and sessions_purchased is accepted as sent. This is what allows an integration to start posting before the packages have been configured.

Processing flow

  1. Validate the JSON schema (types, minimums, sessions_completed ≤ sessions_purchased, date format).

  2. Resolve Contact with LOWER(email) = lower(trim(client_email)). If none → 422 contact_not_found.

  3. Find the contact's orders containing at least one coaching package, newest first. If none → 422 coaching_order_not_found. The rollup is bound to the newest one; the number of coaching orders the email resolved to is recorded on the record for traceability. Multiple matches never reject the payload.

  4. Compute the expected quota for that order. If a quota is configured and it differs from sessions_purchased422 sessions_mismatch; nothing is written.

  5. Recompute sessions_remaining. A value reported by the caller that differs is stored as a discrepancy alongside the record.

  6. Insert or update the rollup for that order (counters, last_session_date, received timestamp, metadata).

  7. Return 200 with the stored rollup.

Unexpected server errors → 500 store_failed (logged server-side).

Success response

HTTP 200 OK

json

{ "data": { "client": { "uuid": "<contact_uuid>", "email": "<email_as_stored>" }, "sessions_purchased": 6, "sessions_completed": 2, "sessions_remaining": 4, "last_session_date": "2026-05-08", "status": "Active" } }
  • sessions_remaining: always the value stored by the server, which may differ from the one sent.

  • last_session_date: YYYY-MM-DD, or null if never reported.

  • status: Completed when sessions_purchased > 0 and sessions_remaining = 0; otherwise Active.

Error responses

Standard envelope, as with every C2C endpoint:

json

{ "error": "<code>", "message": "<message>" }

sessions_mismatch extends the envelope with the two values that disagree, so the caller can correct without a support round trip:

json

{ "error": "sessions_mismatch", "message": "<message>", "expected": 6, "received": 12 }

HTTP

error

Typical cause

HTTP

error

Typical cause

400

missing_api_version

X-Api-Version missing or empty

400

unsupported_api_version

Version other than v1

401

invalid_api_key

Missing/invalid key or wrong prefix

422

validation_failed

Schema, missing field, negative value, sessions_completed greater than sessions_purchased, or bad date format

422

contact_not_found

No contact for the given email

422

coaching_order_not_found

The contact exists but has never bought a coaching package

422

sessions_mismatch

sessions_purchased disagrees with the sessions configured for the purchased package

500

store_failed

Unexpected error while storing progress

User-visible message values may be localized (en / es) depending on server configuration (see resources/lang/*/api_error.php, key c2c_coaching_sessions).

Example (curl)

curl --request POST \ --url 'https://{tenant-host}/api/c2c/coaching/sessions' \ --header 'Authorization: Bearer c2c_YOUR_KEY' \ --header 'Content-Type: application/json' \ --header 'X-Api-Version: v1' \ --data '{ "client_email": "jane@email.com", "sessions_purchased": 6, "sessions_completed": 2, "sessions_remaining": 4, "last_session_date": "2026-05-08" }'

Integrator notes

  • The API does not create contacts; ensure the contact exists first.