Opami developersAPI v1.0.0

API reference

Everything a custom app needs: the endpoints it calls on Opami and the endpoints it implements for Opami to call. Readable without an Opami account. A member creates the custom app and its API key in Opami's settings; the key is shown once.

Version 1.0.0. Server https://app.opami.ai. Authentication: Authorization: Bearer opk_…: opk_ plus 43 base64url characters. Never a session cookie. Download the description: public-v1.yaml.

Rate limits

ThrottleLimitPer
source:ip600 requests / 60 sclient address
source:key120 requests / 60 sAPI key

POST /v1/sources/{id}/messages

Send one message. One message, in the documented shape or in any JSON shape the source's confirmed field mapping can read. A payload in the documented shape is always taken as it is. The message enters the same pipeline as a Gmail message (filters, grouping, summary, learning).

Authentication
API key: Authorization: Bearer opk_…
Body limit
262,144 bytes (413 above)
Rate limits
source:ip, source:key
Idempotency
Idempotent by external_id per source. Sending the same external_id again answers 200 with the first message's id and duplicate: true, and writes nothing. Safe to retry after a timeout or a 503.

Path parameters

idstring (uuid)The custom app's id, shown with its API key.

Request body

FieldTypeRequiredNotes
external_idstringyeslength 1–512. Your id for the message; the idempotency key.
conversation_idstringlength 1–512. Groups messages into one thread.
sent_atstring (date-time) With a time zone offset. Defaults to now.
senderobjectyes Other keys in sender are ignored.
kind"email" | "phone" | "platform_user"default "platform_user".
valuestringyeslength 1–320.
displaystring | nulllength 0–320.
subjectstring | nulllength 0–998.
textstringyeslength 1–∞.
htmlstring | null
source_urlstring | null (uri)
uploadsstring (uuid)[]0–50 items.

Responses

StatusMeaning and body
200This source already sent this external_id. Nothing was written.
FieldTypeRequiredNotes
message_idstring (uuid)yes
duplicatetrueyes
202Accepted.
FieldTypeRequiredNotes
message_idstring (uuid)yes
status"received"yes
401No key, a malformed, unknown or revoked key, a key of another app, a disabled app, an app without this capability, or an id that is not a uuid. All the same answer.
FieldTypeRequiredNotes
errorstringyes An English sentence.
code"unauthenticated"yes
413The body is over the endpoint's limit (x-opami-max-body-bytes).
FieldTypeRequiredNotes
errorstringyes An English sentence.
code"too_large"yes
422The body is not JSON (invalid_json), or it is neither in the documented shape nor readable by the confirmed mapping (unmappable). An unknown or already-used upload id is { field: "uploads", code: "invalid" }.
FieldTypeRequiredNotes
errorstringyes An English sentence.
code"unauthenticated" | "too_large" | "rate_limited" | "unavailable" | "not_found" | "invalid_json" | "unmappable" | "invalid" | "invalid_events" | "calendar_not_usable"yes
errorsobject[]yes
fieldstringyes The target field
pathstring | nullyes The path in your payload the mapping read it from.
code"missing" | "invalid" | "unmapped"yes
429Over the address or the key throttle. Wait Retry-After seconds.
FieldTypeRequiredNotes
errorstringyes An English sentence.
code"rate_limited"yes
503Opami could not complete the request. Retry with backoff.
FieldTypeRequiredNotes
errorstringyes An English sentence.
code"unavailable"yes

Examples

curl
curl -X POST "https://app.opami.ai/v1/sources/$OPAMI_SOURCE_ID/messages" \
  -H "Authorization: Bearer $OPAMI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"external_id":"A-1001","conversation_id":"T-77","sent_at":"2026-10-27T09:00:00Z","sender":{"kind":"email","value":"ana@example.com","display":"Ana Silva"},"subject":"Invoice 2026-114","text":"Hi, please find the October invoice attached.","uploads":["8d0f5b1e-3c1a-4c8e-9b7a-2f4e1d6c9a01"]}'
JavaScript
const res = await fetch(`https://app.opami.ai/v1/sources/${process.env.OPAMI_SOURCE_ID}/messages`, {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.OPAMI_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    "external_id": "A-1001",
    "conversation_id": "T-77",
    "sent_at": "2026-10-27T09:00:00Z",
    "sender": {
      "kind": "email",
      "value": "ana@example.com",
      "display": "Ana Silva"
    },
    "subject": "Invoice 2026-114",
    "text": "Hi, please find the October invoice attached.",
    "uploads": [
      "8d0f5b1e-3c1a-4c8e-9b7a-2f4e1d6c9a01"
    ]
  }),
});
console.log(res.status, await res.json());
Python
import os, requests

res = requests.post(
    f"https://app.opami.ai/v1/sources/{os.environ['OPAMI_SOURCE_ID']}/messages",
    headers={"Authorization": f"Bearer {os.environ['OPAMI_API_KEY']}"},
    json={
        "external_id": "A-1001",
        "conversation_id": "T-77",
        "sent_at": "2026-10-27T09:00:00Z",
        "sender": {
            "kind": "email",
            "value": "ana@example.com",
            "display": "Ana Silva"
        },
        "subject": "Invoice 2026-114",
        "text": "Hi, please find the October invoice attached.",
        "uploads": [
            "8d0f5b1e-3c1a-4c8e-9b7a-2f4e1d6c9a01"
        ]
    },
    timeout=30,
)
print(res.status_code, res.json())

GET /v1/sources/{id}/messages/{message_id}

Read a message's processing status.

Authentication
API key: Authorization: Bearer opk_…
Body limit
No body
Rate limits
source:ip, source:key
Idempotency
A read; safe to repeat.

Path parameters

idstring (uuid)The custom app's id, shown with its API key.
message_idstring (uuid)The message_id answered when the message was sent.

Responses

StatusMeaning and body
200The message's status.
FieldTypeRequiredNotes
message_idstring (uuid)yes
status"received" | "processing" | "done" | "failed"yes received: not picked up yet; processing: on its way to an item; done: on an item the member can see; failed: its job died or its item failed.
401No key, a malformed, unknown or revoked key, a key of another app, a disabled app, an app without this capability, or an id that is not a uuid. All the same answer.
FieldTypeRequiredNotes
errorstringyes An English sentence.
code"unauthenticated"yes
404This source sent no message with this id.
FieldTypeRequiredNotes
errorstringyes An English sentence.
code"not_found"yes
429Over the address or the key throttle. Wait Retry-After seconds.
FieldTypeRequiredNotes
errorstringyes An English sentence.
code"rate_limited"yes
503Opami could not complete the request. Retry with backoff.
FieldTypeRequiredNotes
errorstringyes An English sentence.
code"unavailable"yes

Examples

curl
curl -X GET "https://app.opami.ai/v1/sources/$OPAMI_SOURCE_ID/messages/$MESSAGE_ID" \
  -H "Authorization: Bearer $OPAMI_API_KEY"
JavaScript
const res = await fetch(`https://app.opami.ai/v1/sources/${process.env.OPAMI_SOURCE_ID}/messages/${process.env.MESSAGE_ID}`, {
  method: "GET",
  headers: { Authorization: `Bearer ${process.env.OPAMI_API_KEY}` },
});
console.log(res.status, await res.json());
Python
import os, requests

res = requests.get(
    f"https://app.opami.ai/v1/sources/{os.environ['OPAMI_SOURCE_ID']}/messages/{os.environ['MESSAGE_ID']}",
    headers={"Authorization": f"Bearer {os.environ['OPAMI_API_KEY']}"},
    timeout=30,
)
print(res.status_code, res.json())

POST /v1/sources/{id}/uploads

Announce a file. Answers a signed upload URL. PUT the file's bytes to upload_url, then name upload_id in one message's uploads. An upload attaches to one message only.

Authentication
API key: Authorization: Bearer opk_…
Body limit
4,096 bytes (413 above)
Rate limits
source:ip, source:key
Idempotency
Not idempotent: every call creates a new upload. An upload never attached to a message is never shown.

Path parameters

idstring (uuid)The custom app's id, shown with its API key.

Request body

FieldTypeRequiredNotes
namestringyeslength 1–255.
mimestringyeslength 0–255, pattern ^[\w.+-]+/[\w.+-]+$.
bytesintegeryes0–26214400.

Responses

StatusMeaning and body
201Created.
FieldTypeRequiredNotes
upload_idstring (uuid)yes
upload_urlstring (uri)yes A signed URL; PUT the file there.
401No key, a malformed, unknown or revoked key, a key of another app, a disabled app, an app without this capability, or an id that is not a uuid. All the same answer.
FieldTypeRequiredNotes
errorstringyes An English sentence.
code"unauthenticated"yes
413The body is over the endpoint's limit (x-opami-max-body-bytes).
FieldTypeRequiredNotes
errorstringyes An English sentence.
code"too_large"yes
422Not a valid upload request (invalid).
FieldTypeRequiredNotes
errorstringyes An English sentence.
code"unauthenticated" | "too_large" | "rate_limited" | "unavailable" | "not_found" | "invalid_json" | "unmappable" | "invalid" | "invalid_events" | "calendar_not_usable"yes
errorsobject[]yes
fieldstringyes The target field
pathstring | nullyes The path in your payload the mapping read it from.
code"missing" | "invalid" | "unmapped"yes
429Over the address or the key throttle. Wait Retry-After seconds.
FieldTypeRequiredNotes
errorstringyes An English sentence.
code"rate_limited"yes
503Opami could not complete the request. Retry with backoff.
FieldTypeRequiredNotes
errorstringyes An English sentence.
code"unavailable"yes

Examples

curl
curl -X POST "https://app.opami.ai/v1/sources/$OPAMI_SOURCE_ID/uploads" \
  -H "Authorization: Bearer $OPAMI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"invoice-2026-114.pdf","mime":"application/pdf","bytes":48213}'
JavaScript
const res = await fetch(`https://app.opami.ai/v1/sources/${process.env.OPAMI_SOURCE_ID}/uploads`, {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.OPAMI_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    "name": "invoice-2026-114.pdf",
    "mime": "application/pdf",
    "bytes": 48213
  }),
});
console.log(res.status, await res.json());
Python
import os, requests

res = requests.post(
    f"https://app.opami.ai/v1/sources/{os.environ['OPAMI_SOURCE_ID']}/uploads",
    headers={"Authorization": f"Bearer {os.environ['OPAMI_API_KEY']}"},
    json={
        "name": "invoice-2026-114.pdf",
        "mime": "application/pdf",
        "bytes": 48213
    },
    timeout=30,
)
print(res.status_code, res.json())

POST /v1/sources/{id}/calendar-events

Push changed calendar events. A push is a signal: the events are checked against the event format and the app's calendar is marked changed, so Opami reads it again rather than trusting an older copy. Same API key as the inbox source; the app must offer Calendar. Apps that cannot push are polled through events.list instead.

Authentication
API key: Authorization: Bearer opk_…
Body limit
1,048,576 bytes (413 above)
Rate limits
source:ip, source:key
Idempotency
Idempotent. Pushing the same events again only marks the calendar changed again.

Path parameters

idstring (uuid)The custom app's id, shown with its API key.

Request body

FieldTypeRequiredNotes
eventsEvent[]yes1–500 items.
idstringyeslength 1–1024.
calendar_idstringyeslength 1–1024.
titlestringlength 0–1024, default "".
startstring (date-time)yes
endstring (date-time)yes
all_daybooleandefault false.
status"confirmed" | "tentative" | "cancelled"yes
attendeesobject[]0–500 items, default [].
emailstringyeslength 0–320.
namestring | nulllength 0–256.
response"needsAction" | "accepted" | "declined" | "tentative"default "needsAction".
selfboolean True on the attendee who is the calendar's owner.
locationstring | nulllength 0–2048.
descriptionstring | nulllength 0–65536.
updated_atstring (date-time)yes

Responses

StatusMeaning and body
202Accepted.
FieldTypeRequiredNotes
acceptedintegeryes1–500.
401No key, a malformed, unknown or revoked key, a key of another app, a disabled app, an app without this capability, or an id that is not a uuid. All the same answer.
FieldTypeRequiredNotes
errorstringyes An English sentence.
code"unauthenticated"yes
409The calendar has not passed its conformance test (calendar_not_usable).
FieldTypeRequiredNotes
errorstringyes An English sentence.
code"unauthenticated" | "too_large" | "rate_limited" | "unavailable" | "not_found" | "invalid_json" | "unmappable" | "invalid" | "invalid_events" | "calendar_not_usable"yes
413The body is over the endpoint's limit (x-opami-max-body-bytes).
FieldTypeRequiredNotes
errorstringyes An English sentence.
code"too_large"yes
422Not JSON (invalid_json) or not in the event format (invalid_events), with field paths.
FieldTypeRequiredNotes
errorstringyes An English sentence.
code"unauthenticated" | "too_large" | "rate_limited" | "unavailable" | "not_found" | "invalid_json" | "unmappable" | "invalid" | "invalid_events" | "calendar_not_usable"yes
errorsobject[]yes0–50 items.
pathstringyes
codestringyes
429Over the address or the key throttle. Wait Retry-After seconds.
FieldTypeRequiredNotes
errorstringyes An English sentence.
code"rate_limited"yes
503Opami could not complete the request. Retry with backoff.
FieldTypeRequiredNotes
errorstringyes An English sentence.
code"unavailable"yes

Examples

curl
curl -X POST "https://app.opami.ai/v1/sources/$OPAMI_SOURCE_ID/calendar-events" \
  -H "Authorization: Bearer $OPAMI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"events":[{"id":"evt_123","calendar_id":"team","title":"Call with Ana","start":"2026-11-03T14:00:00+01:00","end":"2026-11-03T14:30:00+01:00","all_day":false,"status":"confirmed","attendees":[{"email":"ana@example.com","name":"Ana Silva","response":"accepted"}],"location":null,"description":null,"updated_at":"2026-10-27T09:12:44Z"}]}'
JavaScript
const res = await fetch(`https://app.opami.ai/v1/sources/${process.env.OPAMI_SOURCE_ID}/calendar-events`, {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.OPAMI_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    "events": [
      {
        "id": "evt_123",
        "calendar_id": "team",
        "title": "Call with Ana",
        "start": "2026-11-03T14:00:00+01:00",
        "end": "2026-11-03T14:30:00+01:00",
        "all_day": false,
        "status": "confirmed",
        "attendees": [
          {
            "email": "ana@example.com",
            "name": "Ana Silva",
            "response": "accepted"
          }
        ],
        "location": null,
        "description": null,
        "updated_at": "2026-10-27T09:12:44Z"
      }
    ]
  }),
});
console.log(res.status, await res.json());
Python
import os, requests

res = requests.post(
    f"https://app.opami.ai/v1/sources/{os.environ['OPAMI_SOURCE_ID']}/calendar-events",
    headers={"Authorization": f"Bearer {os.environ['OPAMI_API_KEY']}"},
    json={
        "events": [
            {
                "id": "evt_123",
                "calendar_id": "team",
                "title": "Call with Ana",
                "start": "2026-11-03T14:00:00+01:00",
                "end": "2026-11-03T14:30:00+01:00",
                "all_day": False,
                "status": "confirmed",
                "attendees": [
                    {
                        "email": "ana@example.com",
                        "name": "Ana Silva",
                        "response": "accepted"
                    }
                ],
                "location": None,
                "description": None,
                "updated_at": "2026-10-27T09:12:44Z"
            }
        ]
    },
    timeout=30,
)
print(res.status_code, res.json())

Error codes

Every error body is { "error": "<sentence>", "code": "<code>" }. Codes: unauthenticated, too_large, rate_limited, unavailable, not_found, invalid_json, unmappable, invalid, invalid_events, calendar_not_usable.