{"openapi":"3.1.1","jsonSchemaDialect":"https://json-schema.org/draft/2020-12/schema","info":{"title":"Postlane Integration API","version":"1.0.0","description":"Server-to-server contact events and transactional email for independent Postlane projects. Create scoped API keys in Project & integration; keep them out of browser/application bundles. These endpoints use project Bearer keys, not administrator cookies or CSRF tokens. Administrator/browser routes and recipient capability links are not part of this integration contract.\n\nAn accepted request is not a delivery receipt. SMTP requires installation-level enabling, Live SMTP in the global/project delivery-mode settings, and valid sender/relay settings. Dry-run is the default, and existing dry-run jobs never become live later. Actual SMTP has ambiguous outcomes; Postlane never promises exactly-once inbox delivery.\n\nSee /help for workflows, consent, scheduling, projects and delivery-log guides. Swagger's Try it out sends real API requests using the key you enter; use synthetic/test contacts and inspect the installation's delivery mode first."},"servers":[{"url":"https://app.postln.app","description":"This Postlane installation"}],"tags":[{"name":"Events","description":"Update contacts and start matching workflows."},{"name":"Transactional","description":"Queue essential account messages from published transactional templates."},{"name":"Operations","description":"Readiness and installation delivery mode."}],"components":{"securitySchemes":{"ProjectApiKey":{"type":"http","scheme":"bearer","bearerFormat":"Opaque project API key","description":"Paste the raw project key; Swagger adds the Bearer prefix. Each operation requires its stated scope. Revoked/wrong-scope keys return 401. Keys never grant access to administrator endpoints."}},"schemas":{"CustomProperties":{"type":"object","propertyNames":{"type":"string","pattern":"^(?!__proto__$|constructor$|prototype$)[a-zA-Z][a-zA-Z0-9_]{0,39}$"},"additionalProperties":{"anyOf":[{"type":"string","maxLength":500},{"type":"number"},{"type":"boolean"},{"type":"null"}]},"maxProperties":30,"description":"A patch of up to 30 scalar custom properties. Null deletes a property. At most 30 properties may remain after merging. Reserved prototype keys are rejected."},"TemplateData":{"type":"object","propertyNames":{"type":"string","pattern":"^(?!__proto__$|constructor$|prototype$)[A-Za-z][A-Za-z0-9_]{0,39}$"},"additionalProperties":{"anyOf":[{"type":"string","maxLength":2000},{"type":"number"},{"type":"boolean"}]},"maxProperties":30,"description":"Template data: up to 30 scalar values, with strings up to 2,000 characters. Null/nested values and reserved prototype names are rejected."},"EventContact":{"type":"object","properties":{"email":{"type":"string","maxLength":254,"format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"name":{"type":"string","maxLength":100},"externalId":{"type":"string","minLength":1,"maxLength":120},"properties":{"$ref":"#/components/schemas/CustomProperties"},"consent":{"type":"boolean"},"consentSource":{"type":"string","minLength":1,"maxLength":300},"newsletter":{"type":"boolean"},"onboarding":{"type":"boolean"},"paid":{"type":"boolean"},"completed":{"type":"boolean"}},"required":["email"],"additionalProperties":false,"allOf":[{"if":{"required":["consent"],"properties":{"consent":{"const":true}}},"then":{"required":["consentSource"]}}],"description":"Email is required; other fields patch existing state. Supply externalId consistently to preserve identity/opt-outs when email changes. Omitted consent/preferences are never opted in. consent=true requires genuine opt-in evidence in consentSource. The API cannot clear unsubscribe or suppression restrictions."},"EventRequest":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-zA-Z0-9_-]{1,80}$"},"name":{"type":"string","pattern":"^[a-zA-Z0-9_-]{1,80}$"},"contact":{"$ref":"#/components/schemas/EventContact"}},"required":["id","name","contact"],"additionalProperties":false},"TransactionalRequest":{"type":"object","properties":{"id":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,80}$"},"templateId":{"type":"string","maxLength":80},"email":{"type":"string","maxLength":254,"format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"name":{"default":"","type":"string","maxLength":100},"data":{"$ref":"#/components/schemas/TemplateData","default":{}},"acknowledgeLiveSend":{"type":"boolean","const":true,"description":"Accepted for compatibility with administrator test sends. This flag does not enable SMTP or bypass installation guards on this API."}},"required":["id","templateId","email"],"additionalProperties":false},"Error":{"type":"object","required":["error"],"additionalProperties":false,"properties":{"error":{"type":"string"}}},"EventAccepted":{"type":"object","required":["duplicate","subscriberId"],"additionalProperties":false,"properties":{"duplicate":{"const":false},"subscriberId":{"type":"string","format":"uuid"},"pending":{"type":"boolean","description":"True when requested marketing consent awaits double-opt-in confirmation."},"blocked":{"type":"boolean","description":"True when unsubscribe/suppression prevents a confirmation request."}}},"EventDuplicate":{"type":"object","required":["duplicate"],"additionalProperties":false,"properties":{"duplicate":{"const":true}}},"TransactionalQueued":{"type":"object","required":["id","duplicate","mode"],"additionalProperties":false,"properties":{"id":{"type":"string","pattern":"^transactional:[A-Za-z0-9_-]{1,80}$"},"duplicate":{"type":"boolean"},"mode":{"type":"string","enum":["dry-run","smtp"],"description":"Frozen when queued. SMTP mode means eligible for SMTP, not already delivered."}}},"Health":{"type":"object","required":["ok","mode"],"additionalProperties":false,"properties":{"ok":{"const":true},"mode":{"type":"string","enum":["dry-run","smtp"]}}}}},"paths":{"/api/events":{"post":{"operationId":"ingestEvent","tags":["Events"],"summary":"Record an event and patch its contact","security":[{"ProjectApiKey":[]}],"x-required-scopes":["events"],"parameters":[{"name":"X-Project-Id","in":"header","required":false,"description":"Normally omit this header: the Bearer key selects the project. If provided, it must equal that key's project ID; mismatches return 403.","schema":{"type":"string"}}],"description":"Requires an events-scoped key. Rate limit: 300 authenticated requests/minute/project. Supply a unique stable id for each occurrence; retry with the same id and exact payload. Duplicates are accepted without another enrollment; changing the payload under an existing id returns 409.\n\nThe name must exactly match an active workflow's Trigger event. Marketing workflows require matching audience, marketing consent and onboarding preference, without unsubscribe/suppression. Transactional account-notice workflows require a key with BOTH events and transactional scopes, matching audience and no suppression; they do not require marketing consent and do not add unsubscribe headers. Events-only keys update the contact and can enroll marketing workflows, but cannot enroll transactional workflows. Transactional workflows use published transactional templates and freeze recipient/name/properties at the event. Workflow frequency is Once per contact by default or Each distinct event; the same event ID still never creates another enrollment. Paused workflows do not accept new enrollments, and existing events are not replayed on activation. Paused transactional workflows also hold their queued messages.\n\nSignup does not itself grant marketing permission. If double opt-in is enabled, new consent stays pending and marketing workflow enrollment begins only after confirmation. Transactional workflows may enroll immediately and are not replayed by the confirmation event. Account-change events update paid/completed properties so existing sequences can exit. This is a generic custom event endpoint; native Stripe/Clerk/etc. webhook payloads must be verified and translated by your application's adapter.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventRequest"},"examples":{"signup":{"summary":"Signup with actual, explicitly recorded opt-in","value":{"id":"signup_user123_v1","name":"signup","contact":{"externalId":"user123","email":"alex@example.invalid","name":"Alex","consent":true,"consentSource":"Getting-started email checkbox selected at signup","onboarding":true,"newsletter":false,"paid":false,"completed":false,"properties":{"plan":"free","seats":1}}}},"accountChanged":{"summary":"Account upgrade stops matching onboarding sequences","value":{"id":"account_user123_v2","name":"account_changed","contact":{"externalId":"user123","email":"alex@example.invalid","paid":true,"properties":{"plan":"paid"}}}},"withdraw":{"summary":"Withdraw consent","value":{"id":"consent_user123_v3","name":"account_changed","contact":{"externalId":"user123","email":"alex@example.invalid","consent":false,"onboarding":false,"newsletter":false}}}}}}},"responses":{"200":{"description":"Event recorded (possibly pending consent), or an exact duplicate accepted. Does not imply that any workflow matched or email was sent.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/EventAccepted"},{"$ref":"#/components/schemas/EventDuplicate"}]},"examples":{"accepted":{"value":{"duplicate":false,"subscriberId":"00000000-0000-4000-8000-000000000123"}},"duplicate":{"value":{"duplicate":true}},"pendingConsent":{"value":{"duplicate":false,"subscriberId":"00000000-0000-4000-8000-000000000123","pending":true}}}}}},"400":{"description":"Invalid JSON/schema, missing opt-in evidence, invalid template data/link, unpublished transactional template, or suppressed recipient.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, revoked, overlong, or wrong-scope project Bearer key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"X-Project-Id conflicts with the project bound to the key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Idempotency ID reused with a different payload, or conflicting contact identity.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"JSON request exceeds 128 KiB.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-project rate limit exceeded. Back off before retrying the same request/ID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal failure. Retry transient errors with the same request ID and payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/transactional":{"post":{"operationId":"queueTransactionalEmail","tags":["Transactional"],"summary":"Queue an essential transactional email","security":[{"ProjectApiKey":[]}],"x-required-scopes":["transactional"],"parameters":[{"name":"X-Project-Id","in":"header","required":false,"description":"Normally omit this header: the Bearer key selects the project. If provided, it must equal that key's project ID; mismatches return 403.","schema":{"type":"string"}}],"description":"Requires a transactional-scoped key; legacy events-only keys are insufficient. Rate limit: 100 authenticated requests/minute/project. The template must belong to this project, have purpose Transactional, and be Published. This API cannot create/edit templates.\n\nUse a stable request id and exact-payload retries. Content/data/header assets are frozen. Reusing an id with different input returns 409; duplicate requests retain the original mode and do not enqueue another job. No workflow is started and marketing consent is never granted. Suppression blocks delivery; essential messages do not include marketing unsubscribe controls. Use only for account-related messages, not newsletters.\n\nProvide all referenced data fields. Text values are escaped/sanitized. A button URL may be exactly {{data.receiptUrl}}, but the resolved destination must be absolute HTTPS with no embedded credentials. Missing/unsafe values reject before queueing. Unknown SMTP outcomes require provider-log review rather than blind retries with a new request id.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransactionalRequest"},"examples":{"receipt":{"summary":"Use the actual ID of your published transactional template","value":{"id":"receipt_order123_v1","templateId":"your-published-template","email":"alex@example.invalid","name":"Alex","data":{"orderNumber":"123","receiptUrl":"https://example.invalid/receipts/123"}}}}}}},"responses":{"200":{"description":"Queued, or an exact duplicate accepted; inspect mode and the administrator Delivery log for actual outcomes.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransactionalQueued"},"example":{"id":"transactional:receipt_order123_v1","duplicate":false,"mode":"dry-run"}}}},"400":{"description":"Invalid JSON/schema, missing opt-in evidence, invalid template data/link, unpublished transactional template, or suppressed recipient.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, revoked, overlong, or wrong-scope project Bearer key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"X-Project-Id conflicts with the project bound to the key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Idempotency ID reused with a different payload, or conflicting contact identity.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"JSON request exceeds 128 KiB.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-project rate limit exceeded. Back off before retrying the same request/ID.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal failure. Retry transient errors with the same request ID and payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/healthz":{"get":{"operationId":"getHealth","tags":["Operations"],"summary":"Check database readiness and installation delivery guard","security":[],"description":"No API key required. Reports the installation guard, not per-project configuration readiness or SMTP connectivity.","responses":{"200":{"description":"Database ready.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"},"example":{"ok":true,"mode":"dry-run"}}}},"500":{"description":"Database unavailable or internal readiness failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}