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 a contact 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 Smith","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 | Retries with the same key return the first response instead of processing again. 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": "010-1234-5678",
"company": "Acme Inc.",
"position": "CEO",
"extra": { "booth": "A-12" }
}'
Response 200
Echoes back exactly what you sent, so you can confirm the write. Fields have the same format as Retrieve contact, except that qr_image_url is always null here β get the QR image URL from Retrieve contact or List contacts.
{
"id": "con_af2f95b4_REG-001",
"object": "external_contact",
"event_id": "af2f95b402794c8f9289360fc8765b8e",
"external_id": "REG-001",
"name": "John Smith",
"email": "john@example.com",
"phone": "010-1234-5678",
"company": "Acme Inc.",
"position": "CEO",
"extra": {"booth": "A-12"},
"created_at": "2026-09-07T04:50:38Z",
"updated_at": "2026-09-07T04:50:38Z",
"qr_image_url": null
}
Errors
| HTTP | code | Cause |
|---|---|---|
| 400 | validation_failed | external_id missing or a field too long |
| 400 | invalid_json | Request body is not valid JSON |
| 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 reused for a different request |
| 409 | idempotency_in_progress | The first request with this Idempotency-Key is still being processed |
| 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" },
{ "name": "Jane Doe", "email": "jane@example.com" }
]
}'
Response 200
The second item in the example has no external_id, so only that item fails and the rest are stored.
{
"total": 2,
"created": 1,
"updated": 0,
"failed": 1,
"errors": [
{
"external_id": null,
"code": "validation_failed",
"message": "external_id is required.",
"param": "external_id"
}
]
}
| Field | Type | Description |
|---|---|---|
| total | integer | Number of items in the request |
| created | integer | Newly created contacts |
| updated | integer | Existing contacts overwritten |
| failed | integer | Items that were not stored |
| errors | array | Failed items only (external_id, code, message, param). Empty when everything succeeds |
Notes
- An empty array or more than 500 items rejects the whole request with 400
validation_failed. - Partial success is allowed β valid items are stored even if others fail. Fix the items in
errorsand resend only those; upsert never creates duplicates. - For large loads, pace your calls; see Rate Limits to avoid 429.
- Supports the
Idempotency-Keyheader β retry a timed-out call with the same key and it is processed only once. See Idempotency.
List contacts
Returns every contact stored for this event in a single response. Use it to reconcile what you have sent, or to collect all QR images at once for badge printing.
GET /api/external/v1/events/{eventId}/contacts
curl https://connectelly.com/api/external/v1/events/<eventId>/contacts \
-H "Authorization: Bearer eyJ..."
Response 200
There are no query parameters and no pagination. Each item in contacts has the same fields as Retrieve contact, and name, email and phone are masked the same way.
{
"total": 2,
"returned": 2,
"truncated": false,
"contacts": [
{
"id": "con_af2f95b4_REG-002",
"object": "external_contact",
"event_id": "af2f95b402794c8f9289360fc8765b8e",
"external_id": "REG-002",
"name": "J******e",
"email": "ja**@example.com",
"phone": null,
"company": null,
"position": null,
"extra": null,
"created_at": "2026-09-07T05:02:11Z",
"updated_at": "2026-09-07T05:02:11Z",
"qr_image_url": "https://cdn.connectelly.com/qr/af2f95b402794c8f9289360fc8765b8e/REG-002.png"
},
{
"id": "con_af2f95b4_REG-001",
"object": "external_contact",
"event_id": "af2f95b402794c8f9289360fc8765b8e",
"external_id": "REG-001",
"name": "J********h",
"email": "jo**@example.com",
"phone": "010-****-5678",
"company": "Acme Inc.",
"position": "CEO",
"extra": {"booth": "A-12"},
"created_at": "2026-09-07T04:50:38Z",
"updated_at": "2026-09-07T04:50:38Z",
"qr_image_url": "https://cdn.connectelly.com/qr/af2f95b402794c8f9289360fc8765b8e/REG-001.png"
}
]
}
| Field | Type | Description |
|---|---|---|
| total | integer | Total number of contacts stored for this event |
| returned | integer | Number of contacts in this response (max 10,000) |
| truncated | boolean | true means the 10,000 limit was hit and only the most recent 10,000 were returned. Contact us if you see this |
| contacts | array | The contacts, most recently registered first |
- Sorted by first registration time (
created_at), newest first. Updating a contact (upsert) does not change its position. - Listing does not create QR images.
qr_image_urlis where the image is stored at registration, so if generation failed back then the link may not open. Retrieve that contact once and the image is created.
Errors
| HTTP | code | Cause |
|---|---|---|
| 404 | event_not_found | Event missing or outside this key's scope |
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",
"event_id": "af2f95b402794c8f9289360fc8765b8e",
"external_id": "REG-001",
"name": "J********h",
"email": "jo**@example.com",
"phone": "010-****-5678",
"company": "Acme Inc.",
"position": "CEO",
"extra": {"booth": "A-12"},
"created_at": "2026-09-07T04:50:38Z",
"updated_at": "2026-09-07T04:50:38Z",
"qr_image_url": "https://cdn.connectelly.com/qr/af2f95b402794c8f9289360fc8765b8e/REG-001.png"
}
| 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.
Response fields
| Field | Type | Description |
|---|---|---|
| id | string | con_ + first 8 characters of event_id + _ + external_id |
| object | string | Always external_contact |
| event_id | string | Event ID (32 characters), same as the eventId in the path |
| external_id | string | The external_id you sent |
| name / email / phone | string | null | Masked value. null if not sent |
| company / position | string | null | As sent. null if not sent |
| extra | object | null | The extra object exactly as you sent it (e.g. {"booth": "A-12"}). null if not sent |
| created_at | string | First registration time, ISO 8601 (UTC). Not changed by later updates |
| updated_at | string | Last update time, ISO 8601 (UTC) |
| qr_image_url | string | null | URL of the badge QR image (PNG, 600Γ600 px). The QR encodes external_id as-is. If the image is missing, this call creates it. null if the image cannot be created |
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.
- Supports the
Idempotency-Keyheader. A retry with the same key replays the first response (204); resending without a key returns 404contact_not_foundbecause the contact is already gone.
Errors
| HTTP | code | Cause |
|---|---|---|
| 404 | contact_not_found | No contact with that external_id |
| 409 | idempotency_conflict | Same Idempotency-Key reused for a different request |
| 409 | idempotency_in_progress | The first request with this Idempotency-Key is still being processed |
Error Codes
All errors follow the Stripe-style format.
{
"error": {
"type": "invalid_request_error",
"code": "validation_failed",
"message": "external_id must be 100 characters or less.",
"param": "external_id",
"doc_url": null,
"request_id": "req_3f9a1c2b7d4e5f60"
}
}
message is always in English and meant for developer logs; it does not change with language settings. Branch your error handling on code, never on message. param names the offending field, and request_id helps us trace a request when you contact support.
| Code | HTTP | Meaning |
|---|---|---|
| validation_failed | 400 | Request validation failed (missing field, too long, too many items, etc.) |
| invalid_json | 400 | Request body is not valid JSON |
| auth_missing | 401 | X-Connectelly-Api-Key header missing |
| auth_invalid | 401 | Key not recognized |
| key_expired | 401 | Key expired (or rotation grace ended) |
| key_revoked | 401 | Key revoked |
| token_missing | 401 | Authorization: Bearer token missing |
| token_invalid | 401 | Token invalid or expired |
| ip_not_allowed | 403 | IP not allowed for this key |
| event_not_found | 404 | Event missing or outside this key's scope |
| contact_not_found | 404 | No contact with that external_id |
| idempotency_conflict | 409 | Same Idempotency-Key reused for a different request (method, path or body) β use a new key |
| idempotency_in_progress | 409 | The first request with this Idempotency-Key is still being processed β retry the same request shortly |
| payload_too_large | 413 | Request body exceeds 2 MB (requests sent with Idempotency-Key) β split it into smaller requests |
| rate_limited | 429 | Rate limit exceeded |
| internal_error | 500 | Internal 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
If a request times out, you cannot tell whether the server already processed it. Send an Idempotency-Key header and retry with the same key: the request is processed only once, and every retry gets the first response back.
Header
| Header | Required | Description |
|---|---|---|
Idempotency-Key | No | 1β255 printable ASCII characters. A UUID v4 is recommended. Generate a new key for each new request |
- An empty key, a key over 255 characters or one with control characters returns 400
validation_failed(param:Idempotency-Key). - Non-ASCII characters (e.g. Korean) may be rejected with 403 before the request reaches the API. Use ASCII only.
- Applies to
POST /contacts,POST /contacts/batchandDELETE /contacts/{externalId}.GETrequests andPOST /tokenignore the header. - Keys are scoped to your API Key β they never collide with another partner's keys.
- A key is kept for 24 hours from its first request. After that, the same key is treated as a new request.
- With this header, a request body over 2 MB returns 413
payload_too_large. A full batch of 500 contacts fits within this limit.
How it works
| Case | Result |
|---|---|
| First request with a key | Processed normally; the response is stored (see below) |
| Same key, same request | Not processed again. The first response's status code and body are returned as-is, with the Idempotent-Replayed: true header |
| Same key, different request | 409 idempotency_conflict β use a new key |
| Same key while the first request is still running | 409 idempotency_in_progress β wait a moment and send the same request again |
- "Same request" means the same method, path, query string and body, byte for byte. Different whitespace or field order counts as a different request, so resend exactly the bytes you sent the first time.
- A replayed body keeps the first request's
request_id; theX-Request-Idresponse header carries a new value for the retry. - Replayed responses also count toward your rate limit.
HTTP/1.1 200
Content-Type: application/json
Idempotent-Replayed: true
X-Request-Id: req_8b2e4d1f0a9c3e75
Which responses are stored
| Response | Retry with the same key |
|---|---|
| 2xx, and client errors such as 400 and 404 | Stored β the same response is replayed |
| 5xx, 429, 408, 409, 401, 403 | Not stored β the request is processed again |
Because a 400 is replayed, use a new key after fixing a request that returned 400. Sending the corrected body with the old key returns 409 idempotency_conflict.
Example: retry after a timeout
Create the key once per request and reuse it on every retry. Here curl retries up to 3 times on timeouts, 408, 429 and 5xx, sending the same key each time. If a retry returns 409 idempotency_in_progress, wait a moment and send the same request again.
KEY=$(uuidgen)
curl -X POST https://connectelly.com/api/external/v1/events/<eventId>/contacts \
-H "Authorization: Bearer eyJ..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
--max-time 10 --retry 3 \
-d '{ "external_id": "REG-001", "name": "John Smith" }'
Changelog
- 2026-10-14 β
Idempotency-Keysupport (create / batch / delete contacts), List contacts API documented, more fields in the Retrieve contact response,extrais returned as an object instead of a string - v1.0 (2026-09) β Initial release: Contacts API (single / batch / retrieve / delete)