API Documentation

Everything you need to connect your registration system to Connectelly.

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.

  1. Apply as a partner and get an API key β€” issued by a Connectelly master after your contract is signed.
  2. Get an access token β€” POST /api/external/v1/token
  3. 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-Key only on POST /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

HeaderRequiredDescription
X-Connectelly-Api-KeyYesYour 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"
}
FieldTypeDescription
access_tokenstringUse in the Authorization header of every later request
token_typestringAlways Bearer
expires_inintegerSeconds remaining. Default 1800 (30 minutes)
expires_atstringExpiry timestamp (ISO-8601 UTC)
event_idstringThe 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_id in 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

HTTPcodeCause
401auth_missingHeader not provided
401auth_invalidKey not recognized
401key_expiredKey expired (or rotation grace ended)
401key_revokedKey was revoked
403ip_not_allowedRequest 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

HeaderRequiredDescription
AuthorizationYesBearer <access_token>
Content-TypeYesapplication/json
Idempotency-KeyNoRetries with the same key return the first response instead of processing again. See Idempotency

Request body

FieldTypeRequiredDescription
external_idstringYesYour system's unique ID, max 100 chars. Must match the QR value
namestringNoFull name, max 200 chars
emailstringNoEmail. Encrypted at rest (AES-256-GCM)
phonestringNoPhone number. Encrypted at rest
companystringNoCompany name, max 200 chars
positionstringNoJob title, max 100 chars
extraobjectNoFree-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

HTTPcodeCause
400validation_failedexternal_id missing or a field too long
400invalid_jsonRequest body is not valid JSON
401token_missing / token_invalidNo token / forged or expired
404event_not_foundEvent missing or outside this key's scope
409idempotency_conflictSame Idempotency-Key reused for a different request
409idempotency_in_progressThe first request with this Idempotency-Key is still being processed
429rate_limitedRate 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

FieldTypeRequiredDescription
contactsarrayYes1–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"
    }
  ]
}
FieldTypeDescription
totalintegerNumber of items in the request
createdintegerNewly created contacts
updatedintegerExisting contacts overwritten
failedintegerItems that were not stored
errorsarrayFailed 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 errors and resend only those; upsert never creates duplicates.
  • For large loads, pace your calls; see Rate Limits to avoid 429.
  • Supports the Idempotency-Key header β€” 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"
    }
  ]
}
FieldTypeDescription
totalintegerTotal number of contacts stored for this event
returnedintegerNumber of contacts in this response (max 10,000)
truncatedbooleantrue means the 10,000 limit was hit and only the most recent 10,000 were returned. Contact us if you see this
contactsarrayThe 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_url is 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

HTTPcodeCause
404event_not_foundEvent 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"
}
StoredReturned by GET
john@example.comjo**@example.com
010-1234-5678010-****-5678
John SmithJ********h
홍길동홍*동

Company, position and extra are not masked. Upsert responses are never masked β€” they echo what you just sent.

Response fields

FieldTypeDescription
idstringcon_ + first 8 characters of event_id + _ + external_id
objectstringAlways external_contact
event_idstringEvent ID (32 characters), same as the eventId in the path
external_idstringThe external_id you sent
name / email / phonestring | nullMasked value. null if not sent
company / positionstring | nullAs sent. null if not sent
extraobject | nullThe extra object exactly as you sent it (e.g. {"booth": "A-12"}). null if not sent
created_atstringFirst registration time, ISO 8601 (UTC). Not changed by later updates
updated_atstringLast update time, ISO 8601 (UTC)
qr_image_urlstring | nullURL 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

HTTPcodeCause
404contact_not_foundNo contact with that external_id
404event_not_foundEvent 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-Key header. A retry with the same key replays the first response (204); resending without a key returns 404 contact_not_found because the contact is already gone.

Errors

HTTPcodeCause
404contact_not_foundNo contact with that external_id
409idempotency_conflictSame Idempotency-Key reused for a different request
409idempotency_in_progressThe 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.

CodeHTTPMeaning
validation_failed400Request validation failed (missing field, too long, too many items, etc.)
invalid_json400Request body is not valid JSON
auth_missing401X-Connectelly-Api-Key header missing
auth_invalid401Key not recognized
key_expired401Key expired (or rotation grace ended)
key_revoked401Key revoked
token_missing401Authorization: Bearer token missing
token_invalid401Token invalid or expired
ip_not_allowed403IP not allowed for this key
event_not_found404Event missing or outside this key's scope
contact_not_found404No contact with that external_id
idempotency_conflict409Same Idempotency-Key reused for a different request (method, path or body) β€” use a new key
idempotency_in_progress409The first request with this Idempotency-Key is still being processed β€” retry the same request shortly
payload_too_large413Request body exceeds 2 MB (requests sent with Idempotency-Key) β€” split it into smaller requests
rate_limited429Rate limit exceeded
internal_error500Internal 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 allowance
  • X-RateLimit-Remaining β€” remaining requests in current window
  • Retry-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

HeaderRequiredDescription
Idempotency-KeyNo1–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/batch and DELETE /contacts/{externalId}. GET requests and POST /token ignore 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

CaseResult
First request with a keyProcessed normally; the response is stored (see below)
Same key, same requestNot processed again. The first response's status code and body are returned as-is, with the Idempotent-Replayed: true header
Same key, different request409 idempotency_conflict β€” use a new key
Same key while the first request is still running409 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; the X-Request-Id response 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

ResponseRetry with the same key
2xx, and client errors such as 400 and 404Stored β€” the same response is replayed
5xx, 429, 408, 409, 401, 403Not 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-Key support (create / batch / delete contacts), List contacts API documented, more fields in the Retrieve contact response, extra is returned as an object instead of a string
  • v1.0 (2026-09) β€” Initial release: Contacts API (single / batch / retrieve / delete)