Getting Started
Step-by-step guide to creating a Bridge app, registering resource types, and running your first sync.
This guide walks you through the complete flow: creating a Bridge app, registering resource types, syncing resources, and verifying the result.
Prerequisites
API key -- Generate one in Settings -> API Keys in your organization's dashboard.
Base URL -- All Bridge API requests use the following base URL, where {tenant_slug} is your organization slug (visible on the API Keys page):
https://developer.idenhq.com/org/{tenant_slug}Authentication -- Pass your API key in the Authorization header on every request:
Authorization: Api-Key <your-api-key>Step 1: Create a Bridge App
Register your external application with Iden.
curl:
curl -X POST "https://developer.idenhq.com/org/acme/api/v1/bridge/apps/" \
-H "Authorization: Api-Key <your-api-key>" \
-H "Content-Type: application/json" \
-d '{"name": "Acme HR"}'Python:
import requests
BASE_URL = "https://developer.idenhq.com/org/acme"
HEADERS = {
"Authorization": "Api-Key <your-api-key>",
"Content-Type": "application/json",
}
response = requests.post(
f"{BASE_URL}/api/v1/bridge/apps/",
headers=HEADERS,
json={"name": "Acme HR"},
)
response.raise_for_status()
app = response.json()
app_id = app["id"]
print(f"Created app: {app_id}")Response (201 Created):
{
"id": "a1b2c3d4-...",
"name": "Acme HR",
"configuration_status": "CHECKING_CONFIGURATION",
"created_at": "2025-01-15T10:00:00Z",
"last_synced_at": null
}Save the id value -- you will use it as {app_id} in all subsequent requests.
Step 2: Register Resource Types
Define what kinds of data your connector will sync. Each resource type needs a unique slug, a display name, and a type (account, group, or license).
Resource types can be registered in any order.
curl:
# Register a group resource type
curl -X POST "https://developer.idenhq.com/org/acme/api/v1/bridge/apps/${APP_ID}/resource-types/" \
-H "Authorization: Api-Key <your-api-key>" \
-H "Content-Type: application/json" \
-d '{"slug": "team", "name": "Team", "type": "group"}'
# Register an account resource type
curl -X POST "https://developer.idenhq.com/org/acme/api/v1/bridge/apps/${APP_ID}/resource-types/" \
-H "Authorization: Api-Key <your-api-key>" \
-H "Content-Type: application/json" \
-d '{"slug": "account", "name": "Account", "type": "account"}'Python:
# Register a group resource type
response = requests.post(
f"{BASE_URL}/api/v1/bridge/apps/{app_id}/resource-types/",
headers=HEADERS,
json={"slug": "team", "name": "Team", "type": "group"},
)
response.raise_for_status()
print(f"Registered resource type: {response.json()['slug']}")
# Register an account resource type
response = requests.post(
f"{BASE_URL}/api/v1/bridge/apps/{app_id}/resource-types/",
headers=HEADERS,
json={"slug": "account", "name": "Account", "type": "account"},
)
response.raise_for_status()
print(f"Registered resource type: {response.json()['slug']}")Response (201 Created):
{
"id": "e5f6a7b8-...",
"slug": "team",
"name": "Team",
"type": "group",
"created_at": "2025-01-15T10:01:00Z"
}If a resource type with the same slug already exists, the API returns 409 Conflict.
Step 3: Start a Sync Session
A sync session groups all your resource pushes into a single atomic operation. Starting a session cancels any previous incomplete sessions for the same app.
curl:
curl -X POST "https://developer.idenhq.com/org/acme/api/v1/bridge/apps/${APP_ID}/sync/" \
-H "Authorization: Api-Key <your-api-key>"Python:
response = requests.post(
f"{BASE_URL}/api/v1/bridge/apps/{app_id}/sync/",
headers=HEADERS,
)
response.raise_for_status()
sync_id = response.json()["sync_id"]
print(f"Started sync session: {sync_id}")Response (201 Created):
{
"sync_id": "d4e5f6a7-..."
}Step 4: Push Resource Data
Push records for each resource type using PUT requests. You can push multiple pages per resource type (up to 100 records per page).
We recommend pushing groups before accounts so that group records have full details when memberships reference them. If you push accounts first, referenced groups will be created with minimal info and filled in when the groups are synced.
Push groups
curl:
curl -X PUT "https://developer.idenhq.com/org/acme/api/v1/bridge/apps/${APP_ID}/sync/${SYNC_ID}/team/" \
-H "Authorization: Api-Key <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"records": [
{"id": "g1", "name": "Engineering"},
{"id": "g2", "name": "Design"},
{"id": "g3", "name": "Marketing"}
]
}'Python:
groups = [
{"id": "g1", "name": "Engineering"},
{"id": "g2", "name": "Design"},
{"id": "g3", "name": "Marketing"},
]
response = requests.put(
f"{BASE_URL}/api/v1/bridge/apps/{app_id}/sync/{sync_id}/team/",
headers=HEADERS,
json={"records": groups},
)
response.raise_for_status()
print(f"Groups synced: {response.json()}")Response (200 OK):
{
"total": 3,
"created": 3,
"updated": 0
}Push accounts with memberships
Account records must include id and either email or username. Use the memberships field to assign accounts to groups -- the key is the group resource type slug, and the value is a list of ref objects with id (and optionally name).
curl:
curl -X PUT "https://developer.idenhq.com/org/acme/api/v1/bridge/apps/${APP_ID}/sync/${SYNC_ID}/account/" \
-H "Authorization: Api-Key <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"records": [
{
"id": "u1",
"email": "alice@acme.com",
"first_name": "Alice",
"last_name": "Smith",
"status": "active",
"memberships": {"team": [{"id": "g1"}, {"id": "g2"}]}
},
{
"id": "u2",
"email": "bob@acme.com",
"first_name": "Bob",
"last_name": "Jones",
"status": "active",
"memberships": {"team": [{"id": "g1"}]}
}
]
}'Python:
accounts = [
{
"id": "u1",
"email": "alice@acme.com",
"first_name": "Alice",
"last_name": "Smith",
"status": "active",
"memberships": {"team": [{"id": "g1"}, {"id": "g2"}]},
},
{
"id": "u2",
"email": "bob@acme.com",
"first_name": "Bob",
"last_name": "Jones",
"status": "active",
"memberships": {"team": [{"id": "g1"}]},
},
]
response = requests.put(
f"{BASE_URL}/api/v1/bridge/apps/{app_id}/sync/{sync_id}/account/",
headers=HEADERS,
json={"records": accounts},
)
response.raise_for_status()
print(f"Accounts synced: {response.json()}")Response (200 OK):
{
"total": 2,
"created": 2,
"updated": 0
}Pagination
If you have more than 100 records for a resource type, push them in multiple pages. Each PUT call appends to the session -- records accumulate across pages.
Python:
def push_records_paginated(app_id, sync_id, slug, all_records, page_size=100):
"""Push records in pages of up to 100."""
for i in range(0, len(all_records), page_size):
page = all_records[i : i + page_size]
response = requests.put(
f"{BASE_URL}/api/v1/bridge/apps/{app_id}/sync/{sync_id}/{slug}/",
headers=HEADERS,
json={"records": page},
)
response.raise_for_status()
result = response.json()
print(f" Page {i // page_size + 1}: {result}")Step 5: Complete the Sync
Once all records have been pushed, complete the session. This triggers an asynchronous cleanup pass that marks any records not included in the session as inactive (removed).
curl:
curl -X POST "https://developer.idenhq.com/org/acme/api/v1/bridge/apps/${APP_ID}/sync/${SYNC_ID}/complete/" \
-H "Authorization: Api-Key <your-api-key>"Python:
response = requests.post(
f"{BASE_URL}/api/v1/bridge/apps/{app_id}/sync/{sync_id}/complete/",
headers=HEADERS,
)
response.raise_for_status()
print("Sync completion triggered (202 Accepted)")Response: 202 Accepted -- the cleanup runs asynchronously.
Step 6: Poll for Completion
After completing a sync, poll the sync status endpoint to check when cleanup finishes.
curl:
curl "https://developer.idenhq.com/org/acme/api/v1/bridge/apps/${APP_ID}/sync/${SYNC_ID}/" \
-H "Authorization: Api-Key <your-api-key>"Python:
import time
while True:
response = requests.get(
f"{BASE_URL}/api/v1/bridge/apps/{app_id}/sync/{sync_id}/",
headers=HEADERS,
)
response.raise_for_status()
status = response.json()
print(f"Status: {status['status']}")
if status["status"] in ("completed", "error", "abandoned"):
break
time.sleep(5)
# Print final progress
for rt in status.get("progress", []):
print(f" {rt['name']}: {rt['synced_count']} records")Response (while running):
{
"sync_id": "d4e5f6a7-...",
"status": "completing",
"progress": [
{"slug": "team", "name": "Team", "type": "group", "synced_count": 3},
{"slug": "account", "name": "Account", "type": "account", "synced_count": 2}
]
}Response (when done):
{
"sync_id": "d4e5f6a7-...",
"status": "completed",
"progress": [
{"slug": "team", "name": "Team", "type": "group", "synced_count": 3},
{"slug": "account", "name": "Account", "type": "account", "synced_count": 2}
]
}Possible status values: in_progress, completing, completed, error, abandoned.
Complete Example
Here is the full flow in a single Python script:
import time
import requests
BASE_URL = "https://developer.idenhq.com/org/acme"
HEADERS = {
"Authorization": "Api-Key <your-api-key>",
"Content-Type": "application/json",
}
def main():
# 1. Create the Bridge app
app = requests.post(
f"{BASE_URL}/api/v1/bridge/apps/",
headers=HEADERS,
json={"name": "Acme HR"},
).json()
app_id = app["id"]
print(f"Created app: {app_id}")
# 2. Register resource types
for rt in [
{"slug": "team", "name": "Team", "type": "group"},
{"slug": "account", "name": "Account", "type": "account"},
]:
resp = requests.post(
f"{BASE_URL}/api/v1/bridge/apps/{app_id}/resource-types/",
headers=HEADERS,
json=rt,
)
resp.raise_for_status()
print(f"Registered: {rt['slug']}")
# 3. Start sync session
sync_id = requests.post(
f"{BASE_URL}/api/v1/bridge/apps/{app_id}/sync/",
headers=HEADERS,
).json()["sync_id"]
print(f"Sync session: {sync_id}")
# 4. Push groups (recommended before accounts)
resp = requests.put(
f"{BASE_URL}/api/v1/bridge/apps/{app_id}/sync/{sync_id}/team/",
headers=HEADERS,
json={
"records": [
{"id": "g1", "name": "Engineering"},
{"id": "g2", "name": "Design"},
]
},
)
resp.raise_for_status()
print(f"Groups: {resp.json()}")
# 5. Push accounts with memberships
resp = requests.put(
f"{BASE_URL}/api/v1/bridge/apps/{app_id}/sync/{sync_id}/account/",
headers=HEADERS,
json={
"records": [
{
"id": "u1",
"email": "alice@acme.com",
"first_name": "Alice",
"last_name": "Smith",
"memberships": {"team": [{"id": "g1"}, {"id": "g2"}]},
},
{
"id": "u2",
"email": "bob@acme.com",
"first_name": "Bob",
"last_name": "Jones",
"memberships": {"team": [{"id": "g1"}]},
},
]
},
)
resp.raise_for_status()
print(f"Accounts: {resp.json()}")
# 6. Complete the sync
requests.post(
f"{BASE_URL}/api/v1/bridge/apps/{app_id}/sync/{sync_id}/complete/",
headers=HEADERS,
).raise_for_status()
print("Sync completing...")
# 7. Poll until done
while True:
status = requests.get(
f"{BASE_URL}/api/v1/bridge/apps/{app_id}/sync/{sync_id}/",
headers=HEADERS,
).json()
print(f" Status: {status['status']}")
if status["status"] in ("completed", "error", "abandoned"):
break
time.sleep(5)
print("Done!")
for rt in status.get("progress", []):
print(f" {rt['name']}: {rt['synced_count']} records")
if __name__ == "__main__":
main()Next Steps
- Sync Guide -- Learn about PUT semantics, multi-page syncs, and the cleanup process.
- Task Execution -- Poll for provisioning tasks and report results.
- Error Reference -- Troubleshoot validation errors and edge cases.