opaige 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
opaige-1.0.0/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Opaige Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the
6
+ "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to
8
+ the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
13
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
14
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
15
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
opaige-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.4
2
+ Name: opaige
3
+ Version: 1.0.0
4
+ Summary: The Opaige API for Python: visa requirements, file review, forms, bookings, lodgements and webhooks.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://opaige.com/developers
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Dynamic: license-file
11
+
12
+ # Opaige for Python
13
+
14
+ The Opaige API from Python 3.8 or later. Standard library only.
15
+
16
+ ```python
17
+ import os
18
+ from opaige import Opaige, OpaigeError, verify_webhook
19
+
20
+ opaige = Opaige(os.environ["OPAIGE_KEY"])
21
+
22
+ answer = opaige.check_requirements(passport="IN", destination="FR")
23
+
24
+ opaige.send_bookings(body={"bookings": [{
25
+ "ref": "QX7K2M", "flight": {"number": "OP412", "destination": "GB", "depart_date": "2026-12-18"},
26
+ "passengers": [{"ref": "QX7K2M-1", "passport": "IN", "residence": "AE"}],
27
+ }]})
28
+
29
+ review = opaige.review_file(file=pdf_bytes, fields={"destination": "FR", "purpose": "business"}, idempotency_key="file-8841")
30
+ ```
31
+
32
+ Every method is one API call, named as in the [API reference](https://opaige.com/developers), in snake_case. JSON goes in `body`;
33
+ a PDF comes back as `bytes`. A query value that is a Python word gets an underscore: `list_invoices(from_="2026-10-01")`.
34
+
35
+ **Errors** raise `OpaigeError` with `status`, `code`, `request_id` (quote it to support) and `details`.
36
+ **Retries:** reads, and writes sent with an `idempotency_key`, are tried again after 429 and 5xx answers (`max_retries`, default 2).
37
+ **Sandbox:** a sandbox key answers in the same shape and saves and bills nothing.
38
+
39
+ ## Webhooks
40
+
41
+ ```python
42
+ event = verify_webhook(request.get_data(), request.headers.get("Opaige-Signature"), os.environ["OPAIGE_WEBHOOK_SECRET"])
43
+ ```
44
+
45
+ Pass the raw body: a parsed and re-serialised body no longer matches its signature.
opaige-1.0.0/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # Opaige for Python
2
+
3
+ The Opaige API from Python 3.8 or later. Standard library only.
4
+
5
+ ```python
6
+ import os
7
+ from opaige import Opaige, OpaigeError, verify_webhook
8
+
9
+ opaige = Opaige(os.environ["OPAIGE_KEY"])
10
+
11
+ answer = opaige.check_requirements(passport="IN", destination="FR")
12
+
13
+ opaige.send_bookings(body={"bookings": [{
14
+ "ref": "QX7K2M", "flight": {"number": "OP412", "destination": "GB", "depart_date": "2026-12-18"},
15
+ "passengers": [{"ref": "QX7K2M-1", "passport": "IN", "residence": "AE"}],
16
+ }]})
17
+
18
+ review = opaige.review_file(file=pdf_bytes, fields={"destination": "FR", "purpose": "business"}, idempotency_key="file-8841")
19
+ ```
20
+
21
+ Every method is one API call, named as in the [API reference](https://opaige.com/developers), in snake_case. JSON goes in `body`;
22
+ a PDF comes back as `bytes`. A query value that is a Python word gets an underscore: `list_invoices(from_="2026-10-01")`.
23
+
24
+ **Errors** raise `OpaigeError` with `status`, `code`, `request_id` (quote it to support) and `details`.
25
+ **Retries:** reads, and writes sent with an `idempotency_key`, are tried again after 429 and 5xx answers (`max_retries`, default 2).
26
+ **Sandbox:** a sandbox key answers in the same shape and saves and bills nothing.
27
+
28
+ ## Webhooks
29
+
30
+ ```python
31
+ event = verify_webhook(request.get_data(), request.headers.get("Opaige-Signature"), os.environ["OPAIGE_WEBHOOK_SECRET"])
32
+ ```
33
+
34
+ Pass the raw body: a parsed and re-serialised body no longer matches its signature.
@@ -0,0 +1,20 @@
1
+ """The Opaige API for Python (2026-11-03). Every method is one API call, named as in the API reference (opaige.com/developers):
2
+
3
+ from opaige import Opaige
4
+ opaige = Opaige(os.environ["OPAIGE_KEY"])
5
+ answer = opaige.check_requirements(passport="IN", destination="FR")
6
+ opaige.send_bookings(body={"bookings": [...]})
7
+
8
+ A sandbox key answers in the same shape and saves and bills nothing. Standard library only.
9
+ """
10
+ from .operations import Operations
11
+ from .request import VERSION, OpaigeError, Requester
12
+ from .webhooks import verify_webhook
13
+
14
+
15
+ class Opaige(Operations):
16
+ def __init__(self, key: str, base_url: str = "https://opaige.com/api/company/v1", timeout: float = 120.0, max_retries: int = 2) -> None:
17
+ super().__init__(Requester(key, base_url=base_url, timeout=timeout, max_retries=max_retries))
18
+
19
+
20
+ __all__ = ["Opaige", "OpaigeError", "verify_webhook", "VERSION"]
@@ -0,0 +1,171 @@
1
+ # Generated by scripts/sdk/generate.mjs from the OpenAPI description (2026-11-01). Do not edit by hand.
2
+
3
+ from typing import Any, Dict, Optional, Union
4
+
5
+ from .request import Requester
6
+
7
+
8
+ class Operations:
9
+ def __init__(self, http: Requester) -> None:
10
+ self._http = http
11
+
12
+ def list_appointments(self, *, date: Optional[Union[str, int]] = None) -> Any:
13
+ """A day’s appointments with the file state and the counter stage. GET /appointments"""
14
+ return self._http.request("GET", f"/appointments", query={"date": date})
15
+
16
+ def send_appointments(self, *, body: Dict[str, Any]) -> Any:
17
+ """Appointments, from a visa centre’s booking system. POST /appointments"""
18
+ return self._http.request("POST", f"/appointments", query=None, body=body)
19
+
20
+ def list_bookings(self, *, updated_since: Optional[Union[str, int]] = None) -> Any:
21
+ """Your bookings with every nudge sent or due. GET /bookings"""
22
+ return self._http.request("GET", f"/bookings", query={"updated_since": updated_since})
23
+
24
+ def send_bookings(self, *, body: Dict[str, Any]) -> Any:
25
+ """Bookings, from an airline’s or travel platform’s reservation system: who needs a visa or travel permission, and when each passenger is nudged. POST /bookings"""
26
+ return self._http.request("POST", f"/bookings", query=None, body=body)
27
+
28
+ def get_booking(self, *, ref: str) -> Any:
29
+ """One booking by your reference. GET /bookings/{ref}"""
30
+ return self._http.request("GET", f"/bookings/{_q(ref)}", query=None)
31
+
32
+ def get_case_file(self, *, id: str) -> Any:
33
+ """A case’s finished file, with fresh links. GET /cases/{id}/file"""
34
+ return self._http.request("GET", f"/cases/{_q(id)}/file", query=None)
35
+
36
+ def resend_case_file(self, *, id: str) -> Any:
37
+ """Send file.ready again. POST /cases/{id}/file"""
38
+ return self._http.request("POST", f"/cases/{_q(id)}/file", query=None)
39
+
40
+ def make_document(self, *, body: Dict[str, Any], format: Optional[Union[str, int]] = None) -> Any:
41
+ """An itinerary, cover letter, submission brief or third-party letter. POST /documents"""
42
+ return self._http.request("POST", f"/documents", query={"format": format}, body=body)
43
+
44
+ def review_file(self, *, body: Optional[Dict[str, Any]] = None, file: Optional[bytes] = None, filename: str = "file.pdf", fields: Optional[Dict[str, str]] = None, idempotency_key: Optional[str] = None) -> Any:
45
+ """Every inconsistency in a whole visa file. POST /file-review"""
46
+ return self._http.request("POST", f"/file-review", query=None, body=body, file=file, filename=filename, fields=fields, idempotency_key=idempotency_key)
47
+
48
+ def get_file_review(self, *, id: str) -> Any:
49
+ """A review made earlier. GET /file-review/{id}"""
50
+ return self._http.request("GET", f"/file-review/{_q(id)}", query=None)
51
+
52
+ def create_upload(self) -> Any:
53
+ """A place to PUT a large PDF before reviewing it. POST /file-review/uploads"""
54
+ return self._http.request("POST", f"/file-review/uploads", query=None)
55
+
56
+ def list_ready_files(self, *, since: Optional[Union[str, int]] = None) -> Any:
57
+ """Every file that became ready since a moment: the daily batch. GET /files"""
58
+ return self._http.request("GET", f"/files", query={"since": since})
59
+
60
+ def fill_form(self, *, body: Dict[str, Any], format: Optional[Union[str, int]] = None) -> Any:
61
+ """The official visa form, filled from answers. POST /forms"""
62
+ return self._http.request("POST", f"/forms", query={"format": format}, body=body)
63
+
64
+ def list_invoices(self, *, status: Optional[Union[str, int]] = None, from_: Optional[Union[str, int]] = None, to: Optional[Union[str, int]] = None) -> Any:
65
+ """Every case billed, due or paid. GET /invoices"""
66
+ return self._http.request("GET", f"/invoices", query={"status": status, "from": from_, "to": to})
67
+
68
+ def list_lodgements(self) -> Any:
69
+ """Your lodgements, newest first. GET /lodgements"""
70
+ return self._http.request("GET", f"/lodgements", query=None)
71
+
72
+ def create_lodgement(self, *, body: Dict[str, Any], idempotency_key: Optional[str] = None) -> Any:
73
+ """Fill a visa application on the government portal, parked before the declarations (au-600) or the photo and signature (us-ds160). POST /lodgements"""
74
+ return self._http.request("POST", f"/lodgements", query=None, body=body, idempotency_key=idempotency_key)
75
+
76
+ def get_lodgement(self, *, id: str) -> Any:
77
+ """Where a lodgement stands, with the government reference once there is one. GET /lodgements/{id}"""
78
+ return self._http.request("GET", f"/lodgements/{_q(id)}", query=None)
79
+
80
+ def attach_lodgement_documents(self, *, id: str, body: Dict[str, Any]) -> Any:
81
+ """Attach supporting documents. POST /lodgements/{id}/documents"""
82
+ return self._http.request("POST", f"/lodgements/{_q(id)}/documents", query=None, body=body)
83
+
84
+ def submit_lodgement(self, *, id: str, body: Dict[str, Any], idempotency_key: Optional[str] = None) -> Any:
85
+ """Lodge and pay the government fee (requires authorize: true). POST /lodgements/{id}/submit"""
86
+ return self._http.request("POST", f"/lodgements/{_q(id)}/submit", query=None, body=body, idempotency_key=idempotency_key)
87
+
88
+ def lodgement_questions(self, *, visa: Optional[Union[str, int]] = None, purpose: Optional[Union[str, int]] = None) -> Any:
89
+ """The questions a DS-160 lodgement answers, as a case asks them. GET /lodgements/questions"""
90
+ return self._http.request("GET", f"/lodgements/questions", query={"visa": visa, "purpose": purpose})
91
+
92
+ def check_passengers(self, *, body: Dict[str, Any]) -> Any:
93
+ """Who on a flight needs a visa, and by when to file. POST /passengers/check"""
94
+ return self._http.request("POST", f"/passengers/check", query=None, body=body)
95
+
96
+ def give_passenger_file(self, *, body: Dict[str, Any], idempotency_key: Optional[str] = None) -> Any:
97
+ """Give a passenger their visa file (billed per file). POST /passengers/file"""
98
+ return self._http.request("POST", f"/passengers/file", query=None, body=body, idempotency_key=idempotency_key)
99
+
100
+ def get_passenger_settings(self) -> Any:
101
+ """Your page for buying the file, and where passengers get help. GET /passengers/settings"""
102
+ return self._http.request("GET", f"/passengers/settings", query=None)
103
+
104
+ def update_passenger_settings(self, *, body: Dict[str, Any]) -> Any:
105
+ """Change them (null clears). PATCH /passengers/settings"""
106
+ return self._http.request("PATCH", f"/passengers/settings", query=None, body=body)
107
+
108
+ def passenger_status(self, *, ref: Optional[Union[str, int]] = None) -> Any:
109
+ """Stages of passengers who chose to share. GET /passengers/status"""
110
+ return self._http.request("GET", f"/passengers/status", query={"ref": ref})
111
+
112
+ def check_requirements(self, *, passport: Optional[Union[str, int]] = None, destination: Optional[Union[str, int]] = None, residence: Optional[Union[str, int]] = None, stay_days: Optional[Union[str, int]] = None) -> Any:
113
+ """What a passport needs for a destination. GET /requirements"""
114
+ return self._http.request("GET", f"/requirements", query={"passport": passport, "destination": destination, "residence": residence, "stay_days": stay_days})
115
+
116
+ def where_to_apply(self, *, residence: Optional[Union[str, int]] = None, destination: Optional[Union[str, int]] = None) -> Any:
117
+ """Who takes the application, where, and who decides it. GET /routes"""
118
+ return self._http.request("GET", f"/routes", query={"residence": residence, "destination": destination})
119
+
120
+ def list_rules(self) -> Any:
121
+ """Every destination the rules cover, with versions. GET /rules"""
122
+ return self._http.request("GET", f"/rules", query=None)
123
+
124
+ def get_rules(self, *, destination: str) -> Any:
125
+ """One destination: every passport grouped by what it needs, with sources. GET /rules/{destination}"""
126
+ return self._http.request("GET", f"/rules/{_q(destination)}", query=None)
127
+
128
+ def list_rule_notices(self, *, since: Optional[Union[str, int]] = None) -> Any:
129
+ """Rule changes announced, in force or coming up. GET /rules/notices"""
130
+ return self._http.request("GET", f"/rules/notices", query={"since": since})
131
+
132
+ def list_students(self) -> Any:
133
+ """Admitted students, with the visa stage where each agreed to share it. GET /students"""
134
+ return self._http.request("GET", f"/students", query=None)
135
+
136
+ def send_students(self, *, body: Dict[str, Any]) -> Any:
137
+ """Admitted students, from a school’s admissions system; each gets a visa link. POST /students"""
138
+ return self._http.request("POST", f"/students", query=None, body=body)
139
+
140
+ def sponsor_compliance(self, *, as_of: Optional[Union[str, int]] = None) -> Any:
141
+ """A UK sponsor’s refusal, enrolment and completion rates as the Basic Compliance Assessment works them. GET /students/compliance"""
142
+ return self._http.request("GET", f"/students/compliance", query={"as_of": as_of})
143
+
144
+ def student_funds_check(self, *, body: Dict[str, Any]) -> Any:
145
+ """Pre-CAS: a UK Student’s money against the Immigration Rules, each finding with its paragraph. POST /students/funds-check"""
146
+ return self._http.request("POST", f"/students/funds-check", query=None, body=body)
147
+
148
+ def list_trips(self, *, status: Optional[Union[str, int]] = None, updated_since: Optional[Union[str, int]] = None) -> Any:
149
+ """Trips and where each case stands. GET /trips"""
150
+ return self._http.request("GET", f"/trips", query={"status": status, "updated_since": updated_since})
151
+
152
+ def create_trip(self, *, body: Dict[str, Any]) -> Any:
153
+ """Plan a trip; it follows your approval rules. POST /trips"""
154
+ return self._http.request("POST", f"/trips", query=None, body=body)
155
+
156
+ def get_trip(self, *, id: str) -> Any:
157
+ """One trip, with its case. GET /trips/{id}"""
158
+ return self._http.request("GET", f"/trips/{_q(id)}", query=None)
159
+
160
+ def get_uptime(self, *, month: Optional[Union[str, int]] = None) -> Any:
161
+ """A month’s measured uptime against the 99.9% commitment. GET /uptime"""
162
+ return self._http.request("GET", f"/uptime", query={"month": month})
163
+
164
+ def get_usage(self, *, month: Optional[Union[str, int]] = None) -> Any:
165
+ """This month’s paid API use and what it comes to. GET /usage"""
166
+ return self._http.request("GET", f"/usage", query={"month": month})
167
+
168
+
169
+ def _q(v: str) -> str:
170
+ from urllib.parse import quote
171
+ return quote(str(v), safe="")
@@ -0,0 +1,71 @@
1
+ """How every call is made: the key, the JSON or the file, retries and errors. Written by hand; the API methods beside it
2
+ (operations.py) are generated from the API description. Standard library only."""
3
+ import json
4
+ import time
5
+ import uuid
6
+ import urllib.error
7
+ import urllib.parse
8
+ import urllib.request
9
+ from typing import Any, Dict, Optional
10
+
11
+ VERSION = "1.0.0"
12
+ _RETRY = {429, 500, 502, 503, 504}
13
+
14
+
15
+ class OpaigeError(Exception):
16
+ def __init__(self, message: str, status: int, code: Optional[str] = None, request_id: Optional[str] = None, details: Any = None):
17
+ super().__init__(message)
18
+ self.status, self.code, self.request_id, self.details = status, code, request_id, details
19
+
20
+
21
+ class Requester:
22
+ def __init__(self, key: str, base_url: str = "https://opaige.com/api/company/v1", timeout: float = 120.0, max_retries: int = 2):
23
+ if not key:
24
+ raise ValueError("An Opaige API key is required: create one in the portal under Developers.")
25
+ self._key, self._base, self._timeout, self._retries = key, base_url.rstrip("/"), timeout, max_retries
26
+
27
+ def request(self, method: str, path: str, query: Optional[Dict[str, Any]] = None, body: Any = None,
28
+ file: Optional[bytes] = None, filename: str = "file.pdf", fields: Optional[Dict[str, str]] = None,
29
+ idempotency_key: Optional[str] = None) -> Any:
30
+ q = {k: v for k, v in (query or {}).items() if v is not None and v != ""}
31
+ url = self._base + path + ("?" + urllib.parse.urlencode(q) if q else "")
32
+ headers = {"Authorization": f"Bearer {self._key}", "User-Agent": f"opaige-python/{VERSION}", "Accept": "application/json"}
33
+ if idempotency_key:
34
+ headers["Idempotency-Key"] = idempotency_key
35
+ data = None
36
+ if file is not None:
37
+ boundary = "opaige" + uuid.uuid4().hex
38
+ parts = [f'--{boundary}\r\nContent-Disposition: form-data; name="{k}"\r\n\r\n{v}\r\n'.encode() for k, v in (fields or {}).items()]
39
+ parts.append(f'--{boundary}\r\nContent-Disposition: form-data; name="file"; filename="{filename}"\r\nContent-Type: application/pdf\r\n\r\n'.encode() + file + b"\r\n")
40
+ data = b"".join(parts) + f"--{boundary}--\r\n".encode()
41
+ headers["Content-Type"] = f"multipart/form-data; boundary={boundary}"
42
+ elif body is not None:
43
+ data = json.dumps(body).encode()
44
+ headers["Content-Type"] = "application/json"
45
+ retryable = method == "GET" or bool(idempotency_key)
46
+ attempt = 0
47
+ while True:
48
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
49
+ try:
50
+ with urllib.request.urlopen(req, timeout=self._timeout) as res:
51
+ raw = res.read()
52
+ if "application/pdf" in (res.headers.get("Content-Type") or ""):
53
+ return raw
54
+ return json.loads(raw or b"{}")
55
+ except urllib.error.HTTPError as e:
56
+ if e.code in _RETRY and retryable and attempt < self._retries:
57
+ wait = e.headers.get("Retry-After")
58
+ time.sleep(min(float(wait), 30.0) if wait and wait.replace(".", "", 1).isdigit() else 0.5 * 2 ** attempt)
59
+ attempt += 1
60
+ continue
61
+ try:
62
+ out = json.loads(e.read() or b"{}")
63
+ except ValueError:
64
+ out = {}
65
+ raise OpaigeError(out.get("error") or f"Opaige answered {e.code}", e.code, out.get("code"), e.headers.get("Opaige-Request-Id"), out.get("details")) from None
66
+ except urllib.error.URLError as e:
67
+ if retryable and attempt < self._retries:
68
+ time.sleep(0.5 * 2 ** attempt)
69
+ attempt += 1
70
+ continue
71
+ raise OpaigeError(f"Could not reach Opaige: {e.reason}", 0, "network_error") from None
@@ -0,0 +1,28 @@
1
+ """Checks that a webhook came from Opaige and was not replayed.
2
+
3
+ Pass the raw request body exactly as received, the Opaige-Signature header and your endpoint's signing secret:
4
+ Opaige-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">
5
+ """
6
+ import hashlib
7
+ import hmac
8
+ import json
9
+ import time
10
+ from typing import Any, Dict, Optional, Union
11
+
12
+
13
+ def verify_webhook(raw_body: Union[str, bytes], signature: Optional[str], secret: str, tolerance_seconds: int = 300, now: Optional[float] = None) -> Dict[str, Any]:
14
+ body = raw_body.decode() if isinstance(raw_body, bytes) else raw_body
15
+ parts = dict(p.strip().split("=", 1) for p in (signature or "").split(",") if "=" in p)
16
+ try:
17
+ t = int(parts.get("t", ""))
18
+ except ValueError:
19
+ raise ValueError("Opaige-Signature is missing or malformed.") from None
20
+ v1 = parts.get("v1")
21
+ if not v1:
22
+ raise ValueError("Opaige-Signature is missing or malformed.")
23
+ if abs((now if now is not None else time.time()) - t) > tolerance_seconds:
24
+ raise ValueError("Opaige-Signature is too old: a replay, or a clock far off.")
25
+ want = hmac.new(secret.encode(), f"{t}.{body}".encode(), hashlib.sha256).hexdigest()
26
+ if not hmac.compare_digest(want, v1):
27
+ raise ValueError("Opaige-Signature does not match this body and secret.")
28
+ return json.loads(body)
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.4
2
+ Name: opaige
3
+ Version: 1.0.0
4
+ Summary: The Opaige API for Python: visa requirements, file review, forms, bookings, lodgements and webhooks.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://opaige.com/developers
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Dynamic: license-file
11
+
12
+ # Opaige for Python
13
+
14
+ The Opaige API from Python 3.8 or later. Standard library only.
15
+
16
+ ```python
17
+ import os
18
+ from opaige import Opaige, OpaigeError, verify_webhook
19
+
20
+ opaige = Opaige(os.environ["OPAIGE_KEY"])
21
+
22
+ answer = opaige.check_requirements(passport="IN", destination="FR")
23
+
24
+ opaige.send_bookings(body={"bookings": [{
25
+ "ref": "QX7K2M", "flight": {"number": "OP412", "destination": "GB", "depart_date": "2026-12-18"},
26
+ "passengers": [{"ref": "QX7K2M-1", "passport": "IN", "residence": "AE"}],
27
+ }]})
28
+
29
+ review = opaige.review_file(file=pdf_bytes, fields={"destination": "FR", "purpose": "business"}, idempotency_key="file-8841")
30
+ ```
31
+
32
+ Every method is one API call, named as in the [API reference](https://opaige.com/developers), in snake_case. JSON goes in `body`;
33
+ a PDF comes back as `bytes`. A query value that is a Python word gets an underscore: `list_invoices(from_="2026-10-01")`.
34
+
35
+ **Errors** raise `OpaigeError` with `status`, `code`, `request_id` (quote it to support) and `details`.
36
+ **Retries:** reads, and writes sent with an `idempotency_key`, are tried again after 429 and 5xx answers (`max_retries`, default 2).
37
+ **Sandbox:** a sandbox key answers in the same shape and saves and bills nothing.
38
+
39
+ ## Webhooks
40
+
41
+ ```python
42
+ event = verify_webhook(request.get_data(), request.headers.get("Opaige-Signature"), os.environ["OPAIGE_WEBHOOK_SECRET"])
43
+ ```
44
+
45
+ Pass the raw body: a parsed and re-serialised body no longer matches its signature.
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ opaige/__init__.py
5
+ opaige/operations.py
6
+ opaige/request.py
7
+ opaige/webhooks.py
8
+ opaige.egg-info/PKG-INFO
9
+ opaige.egg-info/SOURCES.txt
10
+ opaige.egg-info/dependency_links.txt
11
+ opaige.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ opaige
@@ -0,0 +1,19 @@
1
+ [project]
2
+ name = "opaige"
3
+ version = "1.0.0"
4
+ description = "The Opaige API for Python: visa requirements, file review, forms, bookings, lodgements and webhooks."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.8"
9
+ dependencies = []
10
+
11
+ [project.urls]
12
+ Homepage = "https://opaige.com/developers"
13
+
14
+ [build-system]
15
+ requires = ["setuptools>=77"]
16
+ build-backend = "setuptools.build_meta"
17
+
18
+ [tool.setuptools]
19
+ packages = ["opaige"]
opaige-1.0.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+