Quickstart
The Connectelly External API uses two-step authentication. Exchange your API key for a short-lived access token, then call the API with that token.
- Apply as a partner and get an API key β issued by a Connectelly master after your contract is signed.
- Get an access token β
POST /api/external/v1/token - Call with the token β
Authorization: Bearer <access_token>
# 1) Exchange the key for a token (the key is used only here)
curl -X POST https://connectelly.com/api/external/v1/token \
-H "X-Connectelly-Api-Key: ck_live_..."
# β {"access_token":"eyJ...","token_type":"Bearer","expires_in":1800,
# "event_id":"af2f95b4..."} β this is your eventId
# 2) Send contacts with the token
curl -X POST https://connectelly.com/api/external/v1/events/<eventId>/contacts \
-H "Authorization: Bearer eyJ..." \
-H "Content-Type: application/json" \
-d '{"external_id":"REG-001","name":"John","email":"john@example.com"}'
200 means stored. When an app user scans REG-001's QR, a lead is auto-captured.
Authentication
Two-step authentication
Your long-lived API key is used only to obtain a token; all API calls use a short-lived access token. The key never travels with normal traffic, so it cannot leak through partner logs or proxies.
API key
- Format:
ck_live_<32 url-safe base64> - Sent as
X-Connectelly-Api-Keyonly onPOST /api/external/v1/token - Scoped to a single event. Using it for another event returns 404 event_not_found.
- Server-side only. Never embed in client code or commit to public repos.
Access token
- Lives 30 minutes (1800s). Request a new one when it expires.
- Send as
Authorization: Bearer <access_token>on every call. - If the key is revoked, already-issued tokens stop working immediately.
If your key leaks
Tell your Connectelly administrator to revoke it. Revocation instantly blocks the key and every token issued from it. When a replacement key is issued, the old one stays valid for a 24-hour grace period, so you can rotate without downtime.
IP restrictions (recommended)
Register allowed IPs or CIDR ranges and tokens can only be issued from those addresses β a leaked key is unusable elsewhere. Ask your administrator to configure this.
Token API
Exchange your API key for a short-lived access token. This is the only endpoint that accepts the API key; every other endpoint uses the token you get here.
Request
POST /api/external/v1/token
| Header | Required | Description |
|---|---|---|
X-Connectelly-Api-Key | Yes | Your issued API key (ck_live_...) |
No request body is required.
curl -X POST https://connectelly.com/api/external/v1/token \
-H "X-Connectelly-Api-Key: ck_live_..."
Response 200
{
"access_token": "eyJhbGciOiJIUzM4NCJ9...",
"token_type": "Bearer",
"expires_in": 1800,
"expires_at": "2026-09-07T05:18:35Z",
"event_id": "af2f95b402794c8f9289360fc8765b8e"
}
| Field | Type | Description |
|---|---|---|
| access_token | string | Use in the Authorization header of every later request |
| token_type | string | Always Bearer |
| expires_in | integer | Seconds remaining. Default 1800 (30 minutes) |
| expires_at | string | Expiry timestamp (ISO-8601 UTC) |
| event_id | string | The event this key belongs to. Use it for {eventId} in every other endpoint |
Where does eventId come from?
Each API key is bound to a single event, so you never have to track eventId yourself. You can obtain it two ways:
event_idin the token response β returned above. Prefer reading it from here rather than hardcoding.- From your administrator β provided together with the API key.
Using another event's eventId returns 404 event_not_found.
Errors
| HTTP | code | Cause |
|---|---|---|
| 401 | auth_missing | Header not provided |
| 401 | auth_invalid | Key not recognized |
| 401 | key_expired | Key expired (or rotation grace ended) |
| 401 | key_revoked | Key was revoked |
| 403 | ip_not_allowed | Request came from a non-allowlisted IP |
Operational tips
- Cache the token instead of requesting one per call. Refresh it a minute or two before it expires.
- On a 401
token_invalid, fetch a new token and retry once. - Keep tokens in server memory only β never in logs or URLs.
Create / Update contact
Creates a single contact, or overwrites it if the external_id already exists (UPSERT). Call it at registration time so a lead is auto-captured when the app scans the QR.
POST /api/external/v1/events/{eventId}/contacts
Request headers
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer <access_token> |
Content-Type | Yes | application/json |
Idempotency-Key | No | Prevents duplicates on retry. See Idempotency |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| external_id | string | Yes | Your system's unique ID, max 100 chars. Must match the QR value |
| name | string | No | Full name, max 200 chars |
| string | No | Email. Encrypted at rest (AES-256-GCM) | |
| phone | string | No | Phone number. Encrypted at rest |
| company | string | No | Company name, max 200 chars |
| position | string | No | Job title, max 100 chars |
| extra | object | No | Free-form JSON for your own fields |
curl -X POST https://connectelly.com/api/external/v1/events/<eventId>/contacts \
-H "Authorization: Bearer eyJ..." \
-H "Content-Type: application/json" \
-d '{
"external_id": "REG-001",
"name": "John Smith",
"email": "john@example.com",
"phone": "+1-555-000-0000",
"company": "Acme Inc.",
"position": "CEO",
"extra": { "booth": "A-12" }
}'
Response 200
Echoes back exactly what you sent, so you can confirm the write.
{
"id": "con_af2f95b4_REG-001",
"object": "external_contact",
"event_id": "af2f95b4...",
"external_id": "REG-001",
"name": "John Smith",
"email": "john@example.com",
"phone": "+1-555-000-0000",
"company": "Acme Inc.",
"position": "CEO",
"extra": { "booth": "A-12" },
"created_at": "2026-09-07T04:50:38Z",
"updated_at": "2026-09-07T04:50:38Z"
}
Errors
| HTTP | code | Cause |
|---|---|---|
| 400 | validation_failed | external_id missing or too long |
| 401 | token_missing / token_invalid | No token / forged or expired |
| 404 | event_not_found | Event missing or outside this key's scope |
| 409 | idempotency_conflict | Same Idempotency-Key, different body |
| 429 | rate_limited | Rate limit exceeded |
Batch upsert
Upserts up to 500 contacts in one call. Use it for initial loads or nightly syncs.
POST /api/external/v1/events/{eventId}/contacts/batch
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| contacts | array | Yes | 1β500 contacts, each in the single-contact format |
curl -X POST https://connectelly.com/api/external/v1/events/<eventId>/contacts/batch \
-H "Authorization: Bearer eyJ..." \
-H "Content-Type: application/json" \
-d '{
"contacts": [
{ "external_id": "REG-001", "name": "John Smith", "email": "john@example.com" },
{ "external_id": "REG-002", "name": "Jane Doe", "email": "jane@example.com" }
]
}'
Response 200
{
"created": 2,
"updated": 0,
"contacts": [ { "id": "con_...", ... }, { "id": "con_...", ... } ]
}
| Field | Type | Description |
|---|---|---|
| created | integer | Newly created contacts |
| updated | integer | Existing contacts overwritten |
| contacts | array | All processed contacts |
Notes
- An empty array or more than 500 items returns 400
validation_failed. - The batch is all-or-nothing β a partial batch is never stored.
- For large loads, pace your calls; see Rate Limits to avoid 429.
Retrieve contact
Fetches a single stored contact.
GET /api/external/v1/events/{eventId}/contacts/{externalId}
Response 200 β personal data is masked
Name, email and phone come back with the middle characters hidden. This limits the damage if an API key or token leaks: the data cannot be harvested in bulk. You already hold the originals you sent, so day-to-day use is unaffected.
{
"id": "con_af2f95b4_REG-001",
"object": "external_contact",
"external_id": "REG-001",
"name": "J********h",
"email": "jo**@example.com",
"phone": "+1-5**-***-0000",
"company": "Acme Inc.",
"position": "CEO",
"created_at": "2026-09-07T04:50:38Z",
"updated_at": "2026-09-07T04:50:38Z"
}
| Stored | Returned by GET |
|---|---|
| john@example.com | jo**@example.com |
| 010-1234-5678 | 010-****-5678 |
| John Smith | J********h |
Company, position and extra are not masked. Upsert responses are never masked β they echo what you just sent.
Errors
| HTTP | code | Cause |
|---|---|---|
| 404 | contact_not_found | No contact with that external_id |
| 404 | event_not_found | Event missing or outside this key's scope |
Delete contact
Removes a stored contact β use it for cancellations or to correct a duplicate registration.
DELETE /api/external/v1/events/{eventId}/contacts/{externalId}
curl -X DELETE https://connectelly.com/api/external/v1/events/<eventId>/contacts/REG-001 \
-H "Authorization: Bearer eyJ..."
Response 204
Returns 204 No Content with an empty body.
Notes
- After deletion, scanning that QR no longer captures a lead.
- Leads already captured are not deleted β cards collected by app users are kept.
- This cannot be undone. Call the upsert endpoint to add the contact back.
Errors
| HTTP | code | Cause |
|---|---|---|
| 404 | contact_not_found | No contact with that external_id |
Error Codes
All errors follow the Stripe-style format.
{
"error": {
"type": "invalid_request_error",
"code": "external_id_too_long",
"message": "external_id must be 100 characters or less.",
"param": "external_id",
"doc_url": "...",
"request_id": "req_..."
}
}
| Code | HTTP | Meaning |
|---|---|---|
| auth_missing | 401 | Header missing |
| token_missing | 401 | Bearer token missing |
| token_invalid | 401 | Token invalid or expired |
| ip_not_allowed | 403 | IP not permitted |
| auth_invalid | 401 | Key not recognized |
| key_expired | 401 | Key expired |
| key_revoked | 401 | Key revoked |
| event_not_found | 404 | Event missing or out of scope |
| contact_not_found | 404 | Contact missing |
| validation_failed | 400 | Invalid body |
| idempotency_conflict | 409 | Same key, different body |
| rate_limited | 429 | Rate limit exceeded |
| internal_error | 500 | Server error |
Rate Limits
Default 60 req/min per key per task. Connectelly app-api runs 2~6 tasks (auto-scaled), so effective allowance is 120~360 req/min.
Every response includes:
X-RateLimit-Limitβ per-task allowanceX-RateLimit-Remainingβ remaining requests in current windowRetry-Afterβ seconds until retry on 429
Idempotency
To avoid duplicate processing on network retries, send an Idempotency-Key header.
- Up to 100 chars (UUID recommended)
- Cached for 24 hours β retries return the stored response
- Same key with different body returns 409 idempotency_conflict
Changelog
- v1.0 (2026-09) β Initial release: Contacts API (single / batch / retrieve / delete)