Guides

ExpiryEdge API guide

Manage expiries, reminders, contacts and Document Collection requests from your own code. New to APIs? Start with Getting started.

Full reference: openapi.yaml (OpenAPI 3.1 - open it in Swagger UI, Postman or Insomnia).

Contents: Authentication - Base URLs and versioning - Common workflows - Conventions - Errors - Rate limits - Idempotency - Pagination and polling - Webhooks - Deprecation policy - Changelog


Authentication

Send a credential on every call (except public ones such as shared expiry links, the Document Collection fill page, sign-up and password reset):

Authorization: Bearer <api key or token>
CredentialUse forLifetime
API key ee_live_... (recommended)Scripts and integrationsUntil revoked, or its optional expires_at
Session token from POST /loginActing as a person30 days; no refresh token - call /login again on 401
Firebase ID tokenApps using the Firebase Auth SDK1 hour; call user.getIdToken() before each request

API key. An admin creates it in Settings > API keys. It belongs to the organization, uses no seat, and acts as editor (default) or viewer - never admin. You can also send it as X-API-Key: <key>. Step-by-step: API keys.

Session token.

curl -s https://api.expiryedge.com/v1/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"automation@yourcompany.com","password":"<password>"}'
# -> { "accessToken": "eyJhbGciOi...", "user": {...}, "teams": [...] }
  • Wrong email or password returns 401 {"message":"Invalid email or password"}.
  • Changing the password or "Sign out everywhere" invalidates all session tokens.
  • /login is tightly rate limited. Cache the token.

Roles. Every call is limited to your organization's data and your role. Too low a role returns 403. Each endpoint's required role is in its openapi.yaml description.

RoleReadCreate / edit / archiveDelete recordsUsers, billing, settings
ViewerYesNo (comments only)NoNo
EditorYesYesYesNo
AdminYesYesYesYes

Keep keys and tokens out of source control, URLs and logs. If a key leaks, revoke it. If a session token leaks, change the user's password.

Base URLs and versioning

Base URL
Production (use this)https://api.expiryedge.com/v1
Legacy, unversionedhttps://api.expiryedge.com
  • The old address https://us-central1-stylingsphere.cloudfunctions.net/api still works.
  • Endpoints are addressed by name: POST {base}/createExpiry, GET {base}/getExpiry?id=....
  • /v1/ behaves like the unversioned path and adds X-API-Version: 1.
  • Preview: resource-style v2 routes - see Try the v2 API (preview).

Common workflows

All examples assume BASE=https://api.expiryedge.com/v1 and TOKEN=<your API key>.

Create an expiry with reminders

# 1. Create the expiry. `contacts` are contact IDs (getAllContacts).
curl -s "$BASE/createExpiry" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: import-lic-4471' \
  -d '{"name":"Dental license - Northwind","type":"License","expiry_date":"2027-03-31","contacts":["cK9x2..."]}'
# -> includes the new "id"

# 2. Add reminders: 60 and 14 days before, 09:00 org time
curl -s "$BASE/createExpiryReminders" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"expiry_id":"<id>","expiry_date":"2027-03-31","org_timezone":"Europe/Amsterdam",
       "reminders":[{"amount":60,"time_unit":"day","period":"before","time":"09:00","email":true},
                    {"amount":14,"time_unit":"day","period":"before","time":"09:00","email":true}]}'
  • Referenced contacts, assignees and folders must be in your organization, or you get 400 with the IDs in details.
  • Recurring: set is_recurring: true, recurrence_type (daily|weekly|monthly|yearly) and recurrence_interval. Marking it done creates the next one.
  • Send a reminder now: sendExpiryNotificationNow.

Add a contact

curl -s "$BASE/createContact" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: crm-contact-88213' \
  -d '{"firstName":"Priya","lastName":"Shah","email":"priya@northwind.example","phone":"+31612345678"}'

Many at once: bulkImportContacts. Over your plan's contact limit returns 403 with current, limit and remaining.

Send a Document Collection request

# templateId from getAllCollectionTemplates; use groupId instead of contactIds for a contact group
curl -s "$BASE/createCollectionRequests" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: onboarding-northwind-2026-10' \
  -d '{"templateId":"tpl_onboarding","contactIds":["cK9x2..."],"name":"Insurance certificates","dueDate":"2026-11-15"}'
# -> { "created": 1, "emails_sent": 1, "request_ids": ["req_7Hq..."], ... }

Optional expiryId links the collected documents to an expiry. Get results with the submissions feed or a webhook.

Files

Conventions

RequestsJSON bodies (Content-Type: application/json). "Get one" endpoints usually take ?id=.
DatesISO 8601. Calendar dates (expiry_date: "2027-03-31") and reminder time use your organization's timezone. Timestamps (created_at, ...) are UTC.
IDsOpaque strings. Don't parse them.
Server-owned fieldsid, organization_id, user_id, created_at, share_token etc. are ignored if sent.
SuccessTreat any 2xx as success (most creates return 201, a few 200).
Time limitA request can run for at most 60 seconds. Send bulk imports in batches (e.g. 1,000 rows).

Errors

{ "error": "Some of the information provided was invalid.", "code": "VALIDATION_FAILED", "requestId": "a3f09c1be2d4", "details": {} }

error is safe to show users. code is stable. Quote requestId to support. Some older endpoints return only error (or message for /login), so branch on the HTTP status first.

StatuscodeWhat to do
400VALIDATION_FAILED, BAD_REQUESTFix the request.
401UNAUTHORIZED, TOKEN_INVALID, SESSION_EXPIREDGet a new token, retry once.
403FORBIDDEN or plan limit (current/limit/remaining)Role too low or plan limit reached. Don't retry.
404NOT_FOUNDMissing, or in another organization.
405METHOD_NOT_ALLOWEDUse the right HTTP method.
409CONFLICT, IDEMPOTENCY_IN_PROGRESSConflict, or same Idempotency-Key still running - retry after Retry-After.
422IDEMPOTENCY_KEY_REUSEDKey reused with a different body. Use a new key.
429-Wait Retry-After seconds.
5xxINTERNAL_ERROR, TIMEOUTRetry with backoff (and an Idempotency-Key on creates).

Rate limits

Per endpoint and client IP, fixed windows:

TierLimitEndpoints
Strict10 / 15 minlogin, register, sendPasswordResetEmail, verifyCodeAndUpdatePassword, resendVerificationEmail, checkPromoCode, sendSampleReminderEmail, submitSignupRequest
Sensitive30 / 15 mincompleteInvitation, verifyEmail, invite, resendInvitation, sendExpiryNotificationNow
Extraction (AI)30 / minextractDocument, extractDocumentDirect, bulkExtractDocuments, mapImportColumns
High500 / mingetPaginatedExpiries, getExpiryMetrics, getAllExpiries
Default60 / minEverything else

Responses carry X-RateLimit-Remaining. A 429 has Retry-After (seconds) and { "error": "Rate limit exceeded", "retryAfter": 42 }. Plan quotas (expiries, contacts, storage, SMS, AI scans) are separate and return 403.

Idempotency

Send Idempotency-Key: <1-255 chars of A-Z a-z 0-9 - _ : .> on creates so retries don't make duplicates.

  • Supported on: createExpiry, createContact, createContactGroup, createFolder, createComplianceClient, createComplianceProject, createCompliance, createCollectionRequests, createCollectionTemplate, createWorkflowRun, createExpiryComment, createKnowledgeDoc, the file upload-URL and complete-upload endpoints, sendExpiryNotificationNow, bulkImportExpiries, bulkImportContacts, invite, createApiKey (a replay returns "key": null).
  • A 2xx response is stored for 24 hours. Same key + same body returns it again with Idempotent-Replayed: true.
  • Same key, different body: 422 IDEMPOTENCY_KEY_REUSED. Still running: 409 IDEMPOTENCY_IN_PROGRESS.
  • Failed calls aren't stored, so you can fix and retry with the same key.
  • Keys are scoped to your user and endpoint. Derive them from your own record IDs.

Pagination and polling

EndpointPagingUse for
getPaginatedExpiriespage / limit, filters, sortingBrowsing
getAll* (e.g. getAllContacts)Returns everything (some take limit)Small datasets - not polling
listExpiryChanges, listCollectionSubmissionsCursor, oldest first, limit up to 100 (default 50)Sync and polling

Cursor feeds return { data, next_cursor, has_more }. Start with since=<ISO time>, then pass cursor=<next_cursor>. When has_more is false, save next_cursor and poll later.

curl -s "$BASE/listCollectionSubmissions?since=2026-10-01T00:00:00Z&limit=50" -H "Authorization: Bearer $TOKEN"
curl -s "$BASE/listExpiryChanges?event=completed&since=2026-10-01T00:00:00Z" -H "Authorization: Bearer $TOKEN"

listExpiryChanges takes event=created|updated|completed. Download a submission file with getCollectionSubmissionFile; review it with reviewCollectionSubmission.

Webhooks

  • Outgoing webhooks (admins): ExpiryEdge POSTs signed events to your https URL. Events: expiry.created, expiry.updated, expiry.completed, expiry.deleted, collection_request.completed, collection_submission.received. Guide: Get instant updates with webhooks.
  • Teams / Slack reminders: set an incoming-webhook URL in Settings > Integrations (saveWebhooks).
  • Can't receive requests? Use the cursor feeds.

Versioning and deprecation policy

  • Current version: v1. Within v1, changes are additive only (new endpoints, optional fields, response fields, error codes). Ignore unknown fields.
  • Breaking changes ship only in a new version. The old version stays for at least 12 months after the new one is released.
  • Deprecations are marked deprecated: true in openapi.yaml and announced here at least 6 months before removal, with a Deprecation header where possible.
  • Security fixes can ship immediately.

Changelog

2026-09-28

  • Docs published at expiryedge.com/developers/api, with an interactive reference.

2026-09-27 - v1

Added

  • Organization API keys (createApiKey, getApiKeys, revokeApiKey), via Authorization: Bearer ee_live_... or X-API-Key.
  • /v1/ base path (X-API-Version: 1).
  • Idempotency-Key on create and send endpoints.
  • Cursor feeds: listExpiryChanges, listCollectionSubmissions.
  • Expiry comments: getExpiryComments, createExpiryComment, updateExpiryComment, deleteExpiryComment.
  • Expiry attachments: getExpiryAttachments, getExpiryAttachmentUploadUrl, completeExpiryAttachmentUpload, deleteExpiryAttachment.
  • Documents: getFolderFiles, getFolderFileUploadUrl, completeFolderFileUpload, renameFolderFile, moveFolderFile, deleteFolderFile.
  • Docs pages: getKnowledgeDocs, getKnowledgeDoc, createKnowledgeDoc, updateKnowledgeDoc, deleteKnowledgeDoc, getKnowledgeDocAttachments, getKnowledgeDocAttachmentUploadUrl, completeKnowledgeDocAttachmentUpload, deleteKnowledgeDocAttachment.
  • Outgoing webhooks: createWebhookSubscription, getWebhookSubscriptions, updateWebhookSubscription, deleteWebhookSubscription, rotateWebhookSecret, sendTestWebhook, getWebhookDeliveries.
  • Document Collection upload-URL responses include uploadHeaders (send them all with the PUT).
  • getReferralCodeDetails without a token returns public offer terms.
  • updatePassword accepts currentPassword.
  • markExpiryDone accepts the id as id or expiryId in the body.
  • OpenAPI 3.1 reference and these guides.

Changed

  • Auth failures always return 401 (some returned 400/500).
  • References to another organization's contacts, users, folders or directory entries return 400.
  • Deleting expiries and creating share links need Editor or Admin.
  • updateExpiry: empty share_password keeps the current one; null removes it.
  • New rate-limit tiers (sensitive, extraction); password reset and resend-verification moved to strict.
  • Timestamps are always ISO 8601 strings (some endpoints used to return {_seconds, _nanoseconds}).

Security

  • Access-control fixes: organization scoping, role checks on writes, webhook signature verification, internal endpoints closed.