Diese technische Dokumentation ist auf Englisch verfügbar.
Leitfäden

Try the v2 API (preview)

v2 gives every record its own address and uses normal HTTP verbs, one error shape and cursor pages.

  • GET /v2/expiries - list, POST /v2/expiries - create
  • GET, PATCH, DELETE /v2/expiries/{id} - read, change, delete
  • POST /v2/expiries/{id}/complete and /archive

The same pattern covers reminders, attachments, comments, folders, contacts, contact groups, Document Collection and compliance clients - see Step 6 and the Reference. v2 runs the same rules as v1 (plan limits, roles, reminders, workflows).

Preview. Paths and fields may change before GA; changes are announced in the changelog. Everything not listed under Reference is v1 only. For integrations that must not change, use v1.

Before you start

  • Uses the same API key or token as v1 - see API keys.
  • Reading: any role. Creating, changing, completing, archiving, deleting: Editor or Admin.

Note: BASE has no /v1:

export BASE="https://api.expiryedge.com"
export TOKEN="ee_live_..."

Step 1: Create an expiry

Use stored (snake_case) field names. name and expiry_date (YYYY-MM-DD) are required.

curl -s -i -X POST "$BASE/v2/expiries" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: northwind-license-2026" \
  -d '{"name":"Northwind Dental - State Dental License","type":"License","expiry_date":"2026-10-15","contacts":["c_8Hk2pQ"]}'
HTTP/2 201
location: /v2/expiries/exp_4Tq9sLm2
x-api-version: 2

{ "id": "exp_4Tq9sLm2", "name": "...", "expiry_date": "2026-10-15", "state": "todo", "is_done": false, ... }

Step 2: List expiries, one page at a time

curl -s "$BASE/v2/expiries?status=open&sort=expiry_date&limit=50" -H "Authorization: Bearer $TOKEN"
{ "data": [ ... ], "next_cursor": "WyJleHBpcnlfZGF0ZSIs...", "has_more": true }

While has_more is true, send next_cursor back as cursor with the same sort and filters.

ParameterValues
statusopen, done, archived, all (default)
typeexact expiry type, e.g. License
updated_sinceISO 8601 time; needs sort=updated_at or -updated_at
sortexpiry_date (default), -expiry_date, updated_at, -updated_at
limit1-100 (default 50)

Step 3: Change, complete or archive one expiry

# Change only the fields you send
curl -s -X PATCH "$BASE/v2/expiries/exp_4Tq9sLm2" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"notes":"Renewal form sent to the board."}'

# Mark done (list any required checklist items you completed)
curl -s -X POST "$BASE/v2/expiries/exp_4Tq9sLm2/complete" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"closing_notes":"Renewed for 2 years.","checklist_completed":["upload-certificate"]}'

# Archive
curl -s -X POST "$BASE/v2/expiries/exp_4Tq9sLm2/archive" -H "Authorization: Bearer $TOKEN"

Each returns 200 with the expiry. Completing a recurring expiry creates the next one and adds Link: </v2/expiries/...>; rel="next-occurrence".

Step 4: Delete

curl -s -i -X DELETE "$BASE/v2/expiries/exp_4Tq9sLm2" -H "Authorization: Bearer $TOKEN"

Returns 204 No Content. Can't be undone.

Step 5: Contacts

/v2/contacts and /v2/contacts/{id} work the same way.

curl -s -X POST "$BASE/v2/contacts" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"first_name":"Priya","last_name":"Shah","email":"priya@northwind.example","sms_phone":"+15550123456","contact_group":"Licensing"}'
  • Writable: first_name, last_name, email, sms_phone, whatsapp_phone, contact_type, job_title, timezone, contact_group, is_default, email_opt_in, sms_opt_in, whatsapp_opt_in, sendNotifications. Other fields return 400.
  • List filters: type, updated_since, sort (id default, updated_at, -updated_at), cursor, limit.

Step 6: More resources

All follow the same rules: 201 + Location on create, 204 on delete, 404 for another organization's IDs, lists as { data, next_cursor, has_more }.

Reminders on an expiry

curl -s -X POST "$BASE/v2/expiries/exp_4Tq9sLm2/reminders" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"reminders":[{"amount":30,"time_unit":"day","period":"before","time":"09:00"}]}'
  • 1-20 reminders per call, scheduled from the expiry's expiry_date. time uses your org timezone unless you send timezone.
  • GET lists them (soonest first). PATCH with a full reminders list replaces the set (include id to keep one). DELETE removes all (204).

Attachments on an expiry

# 1. Start: returns upload_id, upload_url, headers
curl -s -X POST "$BASE/v2/expiries/exp_4Tq9sLm2/attachments" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"file_name":"License 2026.pdf","content_type":"application/pdf","size":482113}'
# 2. PUT the file to upload_url with exactly the returned headers
curl -s -X PUT "$UPLOAD_URL" -H "Content-Type: application/pdf" -H "x-goog-content-length-range: 0,482113" --data-binary @"License 2026.pdf"
# 3. Complete: returns 201 { index, name, url }
curl -s -X POST "$BASE/v2/expiries/exp_4Tq9sLm2/attachments/complete" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"upload_id":"up_5Nq2rT"}'

GET .../attachments lists files (one page, max 50). DELETE .../attachments?url=<file url> removes one (204).

Comments

curl -s -X POST "$BASE/v2/expiries/exp_4Tq9sLm2/comments" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"text":"Board confirmed the renewal fee."}'

Returns Location: /v2/comments/{id}. List with GET /v2/expiries/{id}/comments (newest first). PATCH /v2/comments/{id} edits your own; DELETE needs the author or an admin. Comments keep app field names (expiryId, userId, timestamp); text is stored HTML-escaped.

Folders

curl -s -X POST "$BASE/v2/folders" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"Dental","parent_folder_id":"fld_2Lq8"}'
  • GET /v2/folders lists flat; filter with parent_folder_id=<id> or root.
  • PATCH renames or moves ("parent_folder_id": null = top level).
  • DELETE on a non-empty folder returns 409 FOLDER_NOT_EMPTY; ?force=true deletes it and moves its expiries out (not deleted).

Contact groups

curl -s -X POST "$BASE/v2/contact-groups" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"Licensing Boards","color":"#1976d2"}'

Fields: name (unique, case-insensitive), color (#rrggbb), sort_order. Contacts belong to a group by contact_group = group name, so renaming renames it on contacts and deleting leaves them ungrouped.

Document Collection

# Templates: GET /v2/collection-templates
curl -s -X POST "$BASE/v2/collection-requests" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: w9-priya-2026" \
  -d '{"template_id":"tpl_W9x2","contact_ids":["c_8Hk2pQ"],"expiry_id":"exp_4Tq9sLm2","name":"W-9 for 2026","due_date":"2026-10-31"}'

# Poll submissions, oldest first
curl -s "$BASE/v2/collection-submissions?submitted_since=2026-09-01T00:00:00Z&limit=100" -H "Authorization: Bearer $TOKEN"
  • Create returns 201 with one request per recipient; each gets an email link.
  • GET /v2/collection-requests filters: status, template_id, contact_id, expiry_id (add sort=id to combine them).
  • POST .../{id}/cancel stops a link. POST .../{id}/resend issues a new link ({"skip_email": true} returns it without emailing).
  • Submissions have responses, review_status and files (metadata only - download with v1 getCollectionSubmissionFile). Filter by review_status or request_id.

Compliance clients

curl -s -X POST "$BASE/v2/compliance-clients" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"Northwind Dental","email":"office@northwind.example","custom_fields":{"tax_id":"12-3456789"}}'

Writable: name, primary_contact_name, email, phone, website, address, industry, status (Active/Inactive), notes, custom_fields. PATCH also takes is_archived. List filters: archived, status. A client with projects can't be deleted (409 CLIENT_HAS_PROJECTS).

Errors - one shape everywhere

{ "error": { "code": "VALIDATION_FAILED", "message": "Some fields are missing or invalid.", "details": { "fields": { "expiry_date": "required, YYYY-MM-DD" } }, "request_id": "9f2c4e1a7b3d" } }

Branch on code, show message, quote request_id to support (also in X-Request-Id).

How v2 differs from v1

v1v2
AddressesPOST /v1/updateExpiry?id=...PATCH /v2/expiries/{id}
VerbsMost accept any methodListed verbs only; others 405 + Allow
Create201/200, { message, id }201, the record, Location
Delete200 { message }204, empty
Other org's record403 or 404Always 404
ErrorsSeveral shapesAlways { error: { code, message, details, request_id } }
ListsWhole collection or pagesCursor, max 100 per page
Field namesContacts camelCaseStored snake_case (except sendNotifications)
Expiry contactsExpanded by getExpiryIDs
share_passwordReturned by getExpiryNever; use has_share_password
Idempotency-KeySelected endpointsEvery POST

Versioning and deprecation

  • v2 stays in preview until GA is announced in the changelog.
  • v1 stays available for at least 12 months after v2 GA, with additive changes only.
  • After GA, v2 follows the Versioning and deprecation policy.

Common problems

SymptomFix
404 "Unknown API path"Use $BASE/v2/... with BASE=https://api.expiryedge.com (no /v1).
405 METHOD_NOT_ALLOWEDUse a verb from the Allow header (PATCH to change).
400 "unknown field"Use stored names (first_name, not firstName).
400 "cursor is invalid"Restart without cursor, same sort.
400 "updated_since requires sort=updated_at"Use sort=updated_at.
400 details.missing_required on completeAdd those checklist ids to checklist_completed.
400 "To combine filters, use sort=id"Add sort=id.
403 PLAN_LIMIT_REACHEDPlan limit used up (details has numbers).
409 EMAIL_EXISTS / GROUP_EXISTS / CONFLICT (folder)Name or email taken. Update the existing record.
409 INVALID_STATECan't cancel a completed request or resend a paused/completed/cancelled one.
404 on /attachments/completeComplete on the same expiry, as the same user that started it.

Reference

Full details: openapi.yaml, "v2 (preview)" tag.

  • GET, POST /v2/expiries; GET, PATCH, DELETE /v2/expiries/{id}; POST /v2/expiries/{id}/complete, /archive
  • GET, POST /v2/contacts; GET, PATCH, DELETE /v2/contacts/{id}
  • GET, POST, PATCH, DELETE /v2/expiries/{id}/reminders
  • GET, POST /v2/expiries/{id}/attachments; POST .../attachments/complete; DELETE .../attachments?url=
  • GET, POST /v2/expiries/{id}/comments; GET, PATCH, DELETE /v2/comments/{id}
  • GET, POST /v2/folders; GET, PATCH, DELETE /v2/folders/{id}
  • GET, POST /v2/contact-groups; GET, PATCH, DELETE /v2/contact-groups/{id}
  • GET /v2/collection-templates, GET /v2/collection-templates/{id} (read-only)
  • GET, POST /v2/collection-requests; GET /v2/collection-requests/{id}; POST .../{id}/cancel, /resend
  • GET /v2/collection-submissions
  • GET, POST /v2/compliance-clients; GET, PATCH, DELETE /v2/compliance-clients/{id}