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>| Credential | Use for | Lifetime |
|---|---|---|
API key ee_live_... (recommended) | Scripts and integrations | Until revoked, or its optional expires_at |
Session token from POST /login | Acting as a person | 30 days; no refresh token - call /login again on 401 |
| Firebase ID token | Apps using the Firebase Auth SDK | 1 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.
/loginis 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.
| Role | Read | Create / edit / archive | Delete records | Users, billing, settings |
|---|---|---|---|---|
| Viewer | Yes | No (comments only) | No | No |
| Editor | Yes | Yes | Yes | No |
| Admin | Yes | Yes | Yes | Yes |
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, unversioned | https://api.expiryedge.com |
- The old address
https://us-central1-stylingsphere.cloudfunctions.net/apistill works. - Endpoints are addressed by name:
POST {base}/createExpiry,GET {base}/getExpiry?id=.... /v1/behaves like the unversioned path and addsX-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
400with the IDs indetails. - Recurring: set
is_recurring: true,recurrence_type(daily|weekly|monthly|yearly) andrecurrence_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
| Requests | JSON bodies (Content-Type: application/json). "Get one" endpoints usually take ?id=. |
| Dates | ISO 8601. Calendar dates (expiry_date: "2027-03-31") and reminder time use your organization's timezone. Timestamps (created_at, ...) are UTC. |
| IDs | Opaque strings. Don't parse them. |
| Server-owned fields | id, organization_id, user_id, created_at, share_token etc. are ignored if sent. |
| Success | Treat any 2xx as success (most creates return 201, a few 200). |
| Time limit | A 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.
| Status | code | What to do |
|---|---|---|
| 400 | VALIDATION_FAILED, BAD_REQUEST | Fix the request. |
| 401 | UNAUTHORIZED, TOKEN_INVALID, SESSION_EXPIRED | Get a new token, retry once. |
| 403 | FORBIDDEN or plan limit (current/limit/remaining) | Role too low or plan limit reached. Don't retry. |
| 404 | NOT_FOUND | Missing, or in another organization. |
| 405 | METHOD_NOT_ALLOWED | Use the right HTTP method. |
| 409 | CONFLICT, IDEMPOTENCY_IN_PROGRESS | Conflict, or same Idempotency-Key still running - retry after Retry-After. |
| 422 | IDEMPOTENCY_KEY_REUSED | Key reused with a different body. Use a new key. |
| 429 | - | Wait Retry-After seconds. |
| 5xx | INTERNAL_ERROR, TIMEOUT | Retry with backoff (and an Idempotency-Key on creates). |
Rate limits
Per endpoint and client IP, fixed windows:
| Tier | Limit | Endpoints |
|---|---|---|
| Strict | 10 / 15 min | login, register, sendPasswordResetEmail, verifyCodeAndUpdatePassword, resendVerificationEmail, checkPromoCode, sendSampleReminderEmail, submitSignupRequest |
| Sensitive | 30 / 15 min | completeInvitation, verifyEmail, invite, resendInvitation, sendExpiryNotificationNow |
| Extraction (AI) | 30 / min | extractDocument, extractDocumentDirect, bulkExtractDocuments, mapImportColumns |
| High | 500 / min | getPaginatedExpiries, getExpiryMetrics, getAllExpiries |
| Default | 60 / min | Everything 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
2xxresponse is stored for 24 hours. Same key + same body returns it again withIdempotent-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
| Endpoint | Paging | Use for |
|---|---|---|
getPaginatedExpiries | page / limit, filters, sorting | Browsing |
getAll* (e.g. getAllContacts) | Returns everything (some take limit) | Small datasets - not polling |
listExpiryChanges, listCollectionSubmissions | Cursor, 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
httpsURL. 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: trueinopenapi.yamland announced here at least 6 months before removal, with aDeprecationheader 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), viaAuthorization: Bearer ee_live_...orX-API-Key. /v1/base path (X-API-Version: 1).Idempotency-Keyon 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). getReferralCodeDetailswithout a token returns public offer terms.updatePasswordacceptscurrentPassword.markExpiryDoneaccepts the id asidorexpiryIdin the body.- OpenAPI 3.1 reference and these guides.
Changed
- Auth failures always return
401(some returned400/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: emptyshare_passwordkeeps the current one;nullremoves 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.
