signedly 0.1.0__py3-none-any.whl
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.
- signedly/__init__.py +92 -0
- signedly/api/__init__.py +14 -0
- signedly/api/api_request_logs_api.py +686 -0
- signedly/api/authentication_api.py +603 -0
- signedly/api/bulk_sending_api.py +858 -0
- signedly/api/documents_api.py +2465 -0
- signedly/api/downloads_api.py +840 -0
- signedly/api/fields_api.py +2308 -0
- signedly/api/sending_api.py +890 -0
- signedly/api/signatories_api.py +1165 -0
- signedly/api/templates_api.py +1452 -0
- signedly/api/webhooks_api.py +2582 -0
- signedly/api_client.py +798 -0
- signedly/api_response.py +21 -0
- signedly/configuration.py +596 -0
- signedly/exceptions.py +200 -0
- signedly/helpers.py +89 -0
- signedly/models/__init__.py +66 -0
- signedly/models/create_bulk_send202_response.py +105 -0
- signedly/models/create_bulk_send_request.py +101 -0
- signedly/models/create_document_fields_request_inner.py +156 -0
- signedly/models/create_document_from_template200_response.py +101 -0
- signedly/models/create_document_from_template_request.py +115 -0
- signedly/models/create_document_from_template_request_recipients_inner.py +92 -0
- signedly/models/create_signatories_request.py +100 -0
- signedly/models/create_signatories_request_signatories_inner.py +92 -0
- signedly/models/create_signatory_request.py +94 -0
- signedly/models/create_template_fields_request_inner.py +156 -0
- signedly/models/create_webhook_endpoint_request.py +103 -0
- signedly/models/delete_field_request.py +88 -0
- signedly/models/document.py +159 -0
- signedly/models/document_page.py +113 -0
- signedly/models/document_status_page.py +113 -0
- signedly/models/document_status_row.py +90 -0
- signedly/models/fill_and_send_document_request.py +116 -0
- signedly/models/fill_and_send_document_request_elements_inner.py +96 -0
- signedly/models/get_access_token_request.py +90 -0
- signedly/models/get_audit_trail_link200_response.py +88 -0
- signedly/models/get_download_status200_response.py +90 -0
- signedly/models/list_bulk_send_columns200_response_inner.py +92 -0
- signedly/models/model_field.py +202 -0
- signedly/models/page.py +102 -0
- signedly/models/refresh_access_token200_response.py +99 -0
- signedly/models/refresh_access_token_request.py +88 -0
- signedly/models/request_count_page.py +113 -0
- signedly/models/request_hit.py +93 -0
- signedly/models/request_log.py +108 -0
- signedly/models/request_log_page.py +113 -0
- signedly/models/send_document200_response.py +88 -0
- signedly/models/send_document_by_link200_response.py +90 -0
- signedly/models/send_document_by_link_request.py +91 -0
- signedly/models/send_document_request.py +102 -0
- signedly/models/signatory.py +119 -0
- signedly/models/template.py +124 -0
- signedly/models/template_field.py +202 -0
- signedly/models/template_page.py +113 -0
- signedly/models/update_document_field_by_body_request.py +158 -0
- signedly/models/update_document_field_by_body_request_items_inner.py +92 -0
- signedly/models/update_webhook_endpoint_request.py +105 -0
- signedly/models/upload.py +110 -0
- signedly/models/upload_document_for_one_signer200_response.py +90 -0
- signedly/models/upload_document_for_one_signer_request.py +96 -0
- signedly/models/upload_document_request.py +101 -0
- signedly/models/webhook_delivery.py +140 -0
- signedly/models/webhook_delivery_page.py +113 -0
- signedly/models/webhook_endpoint.py +116 -0
- signedly/models/webhook_endpoint_with_secret.py +120 -0
- signedly/py.typed +0 -0
- signedly/rest.py +258 -0
- signedly-0.1.0.dist-info/LICENSE +21 -0
- signedly-0.1.0.dist-info/METADATA +18 -0
- signedly-0.1.0.dist-info/RECORD +74 -0
- signedly-0.1.0.dist-info/WHEEL +5 -0
- signedly-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Signedly API
|
|
5
|
+
|
|
6
|
+
The Signedly REST API lets your system upload documents, add signatories and fields, send them for signing and download the signed result. Every path starts with `/ap1/`, and every request and response body is JSON unless an endpoint says otherwise (the uploads are `multipart/form-data`). ## Authentication 1. In the Signedly web app, open **Account & Settings → API** and generate a **client ID** and **client secret** for your organisation. 2. `POST /ap1/token/` with them. You get an `access_token` (valid **2 hours**) and a `refresh_token` (valid **7 days**). While both are still valid, calling `/ap1/token/` again returns the same pair. 3. Send the access token on every other call: `Authorization: Bearer <access_token>`. 4. When the access token expires, `POST /ap1/refreshtoken/` with the refresh token for a new access token. When the refresh token has expired too, call `/ap1/token/` again. `/ap1/token/` and `/ap1/refreshtoken/` allow 60 calls a minute per IP address; more answer 429. A missing, unknown or expired access token answers 403 (`\"Client not found\"` or `\"Access token expired\"`). The API acts for your **organisation's admin account**: documents you upload belong to it and appear in its Signedly web app, and you can reach only that account's documents, templates, signatories and fields. Another organisation's IDs answer 404, as if they didn't exist. ## Plans What the API allows depends on the Signedly plan of your **organisation's admin account** (Account & Settings → Account & Plan). The same rules apply to live and test keys. | What | Free | Personal Pro | Business Pro | Teams | |---|---|---|---|---| | Generate live and test keys (Account & Settings → API) | – | ✓ | ✓ | ✓ | | Every endpoint, `/ap1/token/` and `/ap1/refreshtoken/` included | – | ✓ | ✓ | ✓ | | `require_email_otp` on `POST /ap1/signatories/` and `POST /ap1/signatories/bulk/` | – | ✓ | ✓ | ✓ | | Bulk send: `GET /ap1/bulk-sends/columns/` and `POST /ap1/bulk-sends/` | – | – | – | ✓ | The API is part of the paid plans. On Free (or with no plan) every call answers 403 `\"... Your plan doesn't include API access.\"`, with live or test keys, also with a token or keys made earlier; they work again once the plan is paid. On a paid plan, a feature it doesn't include answers 400 with `\"Your plan doesn't include ...\"` and changes nothing, and `GET /ap1/bulk-sends/{id}/` still reports bulk sends already made. Live documents also need an API contract with Signedly. Without one, or with an unpaid API invoice, `POST /ap1/upload/` (except templates), `/ap1/upload_app/`, `/ap1/documents/fromtemplate/` and `/ap1/bulk-sends/` answer 402 with live keys. Test keys work without a contract (see Sandbox). ## Sending a document for signing 1. `POST /ap1/upload/` a PDF or DOCX (`type` 2). The document is prepared in the background: find it in `GET /ap1/documents/?name=<name>` (its `upload.id` is the upload's `id`), and wait until `GET /ap1/documents/{id}/pages/` lists its pages. 2. `POST /ap1/signatories/` (one) or `POST /ap1/signatories/bulk/` (several) to add the people who sign. 3. `POST /ap1/documents/fields/` to place the fields each signatory fills in. 4. Send it: - `POST /ap1/document/send/` emails every signatory a signing link (with ordered signing, only the first; each next one is emailed in turn), or - `POST /ap1/document/send_url/` sends no email and returns the first signatory's signing link, for you to show or send yourself. 5. Poll `GET /ap1/documents-status-list/` (or `GET /ap1/documents/`) until the status is 6 (Completed), then `GET /ap1/document/download-link/` for the signed PDF and `GET /ap1/document/download-audit-link/` for its audit trail. **From a template:** upload with `type` 4 and a `template_name` (or make the template in the web app), place its fields with `POST /ap1/templates/fields/`, then for each signing make a document with `POST /ap1/documents/fromtemplate/`, add its signatory, and send it. **One signer, one call:** `POST /ap1/upload_app/` uploads a PDF or DOCX for one signer and returns their signing link in `redirect_url`; the signer places their own signature. After signing they go to the `redirect_url` you sent, and the signed PDF and its audit trail are emailed to `final_email` (otherwise to your organisation's admin). ## Quick start with curl Each step uses an ID from the step before. `$API` is this server, e.g. `https://l0ads.signedly.com`. ```bash # 1. Token curl -X POST $API/ap1/token/ -H 'Content-Type: application/json' \\ -d '{\"client_id\": \"<client id>\", \"client_secret\": \"<client secret>\"}' TOKEN=<access_token from the answer> # 2. Upload a PDF (answers with the upload's id) curl -X POST $API/ap1/upload/ -H \"Authorization: Bearer $TOKEN\" \\ -F file=@nda.pdf -F name=nda-ann-lee -F type=2 # 3. Find the document, then wait until it has pages curl \"$API/ap1/documents/?name=nda-ann-lee\" -H \"Authorization: Bearer $TOKEN\" DOC=<results[0].id> curl $API/ap1/documents/$DOC/pages/ -H \"Authorization: Bearer $TOKEN\" PAGE=<[0].id> # 4. Add the signatory curl -X POST $API/ap1/signatories/ -H \"Authorization: Bearer $TOKEN\" \\ -H 'Content-Type: application/json' \\ -d '{\"document_id\": \"'$DOC'\", \"name\": \"Ann Lee\", \"email\": \"ann@example.com\"}' SIGNER=<id from the answer> # 5. Place her signature: 150 × 40 points, 72 points from the left, 640 from the top curl -X POST $API/ap1/documents/fields/ -H \"Authorization: Bearer $TOKEN\" \\ -H 'Content-Type: application/json' \\ -d '[{\"document_id\": \"'$DOC'\", \"signatory_id\": \"'$SIGNER'\", \"kind\": 2, \"label\": \"Signature\", \"placeholder\": \"Sign here\", \"page\": \"'$PAGE'\", \"page_number\": 0, \"x\": 72, \"y\": 640, \"width\": 150, \"height\": 40}]' # 6. Send it (Ann gets the signing email) curl -X POST $API/ap1/document/send/ -H \"Authorization: Bearer $TOKEN\" \\ -H 'Content-Type: application/json' \\ -d '{\"document_id\": \"'$DOC'\", \"subject\": \"Please sign the NDA\", \"message\": \"Hi Ann, please sign by Friday.\", \"expiry_date\": \"2026-12-31\"}' # 7. Poll until status is 6 (Completed), then get the signed PDF curl \"$API/ap1/documents-status-list/?document_ids=$DOC\" -H \"Authorization: Bearer $TOKEN\" curl \"$API/ap1/document/download-link/?id=$DOC\" -H \"Authorization: Bearer $TOKEN\" ``` ## Postman Import the whole API into Postman with **Import → Link** and `<this server>/swaggerap1/postman.json`. Set `client_id` and `client_secret` in the collection's **Variables**, run **Get an access token** (it saves the token for every other request), and set `document_id` when a request needs one. ## Webhooks Instead of polling, add an endpoint with `POST /ap1/webhooks/` (or in **Account & Settings → API**) and Signedly posts your organisation's document events to it: `document.sent`, `document.viewed`, `document.signed`, `document.approved`, `document.completed`, `document.rejected` and `document.expired` (all of them, or the `events` you choose). Each is a JSON `POST`: ```json {\"id\": \"5b7f…\", \"type\": \"document.signed\", \"created_at\": \"2026-10-27T09:12:00+00:00\", \"test\": false, \"data\": {\"document\": {\"id\": \"6f0c…\", \"name\": \"nda.pdf\", \"status\": 5}, \"recipient\": {\"id\": \"0b6e…\", \"email\": \"ann@example.com\", \"name\": \"Ann Lee\", \"role\": \"signer\"}}} ``` `recipient` is null for events no recipient caused (sent, completed, expired); `test` is true for documents made with your test keys. Fetch anything else, such as download links, with your access token: events never carry links. **Check every event is from Signedly.** The `Signedly-Signature` header is `t=<unix seconds>,v1=<signature>`, where the signature is the hex HMAC-SHA256 of `<t>.<raw body>` with the endpoint's secret. Refuse an event whose signature doesn't match or whose `t` is more than 5 minutes off: ```python import hashlib, hmac, time def is_from_signedly(secret, header, raw_body): parts = dict(item.split(\"=\", 1) for item in header.split(\",\")) signed = f\"{parts['t']}.{raw_body.decode()}\".encode() expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() fresh = abs(time.time() - int(parts[\"t\"])) <= 300 return fresh and hmac.compare_digest(parts[\"v1\"], expected) ``` **Answer 2xx within 10 seconds**, then do the work. Anything else is retried after 1 and 5 minutes, then 30 minutes and 2, 5 and 10 hours (7 tries in about 18 hours). `Signedly-Event-Id` is the same on every try of an event: use it to ignore repeats. After 20 events in a row fail for good, Signedly switches the endpoint off and emails your organisation's admin; switch it on again with `PUT /ap1/webhooks/{id}/` and `\"is_active\": true`. Each endpoint's deliveries and their last answers are at `GET /ap1/webhooks/{id}/deliveries/`, and any can be sent again. ## Sandbox Build and test against the API without paying for documents: in **Account & Settings → API**, generate your **test keys**, and get a token with them at `/ap1/token/` as usual. Everything works the same, except that what you upload or make from a template with a test token is a **test document** (`is_test` on its upload): - every page of its PDF and of its audit trail says **TEST – NOT LEGALLY BINDING**; - its emails are real, with `[TEST]` at the start of the subject; - it isn't billed or counted toward your plan; - your organisation can make 50 a calendar month (templates don't count); the next upload answers 429. ## Fields Positions and sizes are in **PDF points** (1/72 inch), measured from the **top-left corner** of the page as it is displayed (rotation applied). `page_number` counts pages from 0. A field must sit wholly on its page (1 point of tolerance); otherwise it is refused with 400. A field's page and signatory can't be changed later: delete the field and add it again. ## Status values **Document `status`:** 1 Draft · 2 Signees Added · 3 Prepared · 4 Sent · 5 In Progress · 6 Completed · 7 Rejected · 8 Archived · 9 Expired. Signatories, fields and sends are only accepted while a document is 1–3 (not yet sent). **Signatory `status`:** 1 Waiting · 2 Invited · 3 Signed · 4 Viewed · 5 Rejected. ## Lists `GET /ap1/documents/`, `/ap1/documents-status-list/`, `/ap1/templates/`, `/ap1/logs/` and `/ap1/logs/hits/` are paged: pass `limit` (default 10) and `offset`, and read `count`, `next`, `previous` and `results` from the response. ## Errors | Status | Meaning | |---|---| | 400 | The request was refused, also when your plan doesn't include it (see Plans); the body says why (`{\"error\": \"...\"}`, or the name of each bad field with its messages). Nothing was changed. | | 403 | The access token is missing, unknown or expired; the client ID and secret or the refresh token were refused; the organisation's plan has no API access (Free, see Plans); or an `expiry_date` isn't in the future (`\"Invalid Date Entered\"`). | | 402 | No API contract, or an unpaid API invoice: live documents are refused (test keys keep working). | | 404 | The document, template, signatory or field doesn't exist or isn't yours. | | 429 | Too many token requests (wait a minute), or this month's 50 test documents are used. | | 5xx | Our side failed; retry later. | A send that times out may still have worked: check the document's status before sending again (a second send of a sent document answers 400). Download, view and audit links are valid for about 7 days; ask for a new one whenever you need it.
|
|
7
|
+
|
|
8
|
+
The version of the OpenAPI document: v1
|
|
9
|
+
Contact: info@signedly.com
|
|
10
|
+
Generated by OpenAPI Generator (https://openapi-generator.tech)
|
|
11
|
+
|
|
12
|
+
Do not edit the class manually.
|
|
13
|
+
""" # noqa: E501
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
import pprint
|
|
18
|
+
import re # noqa: F401
|
|
19
|
+
import json
|
|
20
|
+
|
|
21
|
+
from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictFloat, StrictInt, StrictStr, field_validator
|
|
22
|
+
from typing import Any, ClassVar, Dict, List, Optional, Union
|
|
23
|
+
from signedly.models.update_document_field_by_body_request_items_inner import UpdateDocumentFieldByBodyRequestItemsInner
|
|
24
|
+
from typing import Optional, Set
|
|
25
|
+
from typing_extensions import Self
|
|
26
|
+
|
|
27
|
+
class CreateTemplateFieldsRequestInner(BaseModel):
|
|
28
|
+
"""
|
|
29
|
+
CreateTemplateFieldsRequestInner
|
|
30
|
+
""" # noqa: E501
|
|
31
|
+
align: Optional[StrictInt] = Field(default=None, description="Text: 0 Left · 1 Center · 2 Right.")
|
|
32
|
+
checked: Optional[StrictBool] = Field(default=False, description="Checkbox: true starts it ticked.")
|
|
33
|
+
date_format: Optional[StrictInt] = Field(default=None, description="Date: 1 dd_mm_yyyy · 2 mm_dd_yyyy · 3 dd mmm yyyy · 4 dd-mm-yyyy.")
|
|
34
|
+
document_id: StrictStr = Field(description="The template's ID.")
|
|
35
|
+
font: Optional[StrictStr] = Field(default='Roboto', description="Text, Date and Dropdown: the font.")
|
|
36
|
+
font_size: Optional[StrictInt] = Field(default=None, description="Text and Dropdown: the font size in points. Date fields always use 10.")
|
|
37
|
+
formula: Optional[StrictStr] = Field(default=None, description="Text field: makes it a calculated field, worked out when the document is sent from other fields by their labels in brackets, e.g. `round([CTC] * 40%)` or `if([Grade] = \"A\", 50000, 20000)`. Refused if a name is unknown or the formulas loop.")
|
|
38
|
+
group_name: Optional[StrictStr] = Field(default=None, description="Radio: required; the name of the option group.")
|
|
39
|
+
height: StrictInt = Field(description="In PDF points (1/72 inch), whole: the field's height (each option's, for Radio).")
|
|
40
|
+
is_multi_line: Optional[StrictBool] = Field(default=False, description="Text: true lets the signer type several lines.")
|
|
41
|
+
is_required: Optional[StrictBool] = Field(default=True, description="False lets the signer leave it empty.")
|
|
42
|
+
items: Optional[List[UpdateDocumentFieldByBodyRequestItemsInner]] = Field(default=None, description="Radio: required; the options, at least two, each `width` x `height`. `selected` 1 starts that option chosen.")
|
|
43
|
+
kind: StrictInt = Field(description="The field's type: 1 Text · 2 Signature · 3 Date · 5 Checkbox · 6 Radio · 9 Dropdown · 10 Sign Stamp.")
|
|
44
|
+
label: StrictStr = Field(description="The field's name, shown to the signer.")
|
|
45
|
+
max_length: Optional[StrictInt] = Field(default=None, description="Text: the most characters the signer may type (1–550).")
|
|
46
|
+
number_format: Optional[Dict[str, Any]] = Field(default=None, description="How a calculated number is shown, e.g. {\"kind\": \"number\", \"style\": \"indian\", \"decimals\": 2} or {\"kind\": \"words\", \"currency\": \"INR\"}.")
|
|
47
|
+
page: StrictStr = Field(description="The ID of the page the field is on (GET /ap1/documents/{id}/pages/).")
|
|
48
|
+
page_number: StrictInt = Field(description="That page's number, counting from 0 (the first page is 0).")
|
|
49
|
+
placeholder: StrictStr = Field(description="Hint text shown while the field is empty. Send an empty string for Checkbox and Radio.")
|
|
50
|
+
readonly: Optional[StrictBool] = Field(default=False, description="True: the signer can't change the field's value.")
|
|
51
|
+
select_options: Optional[List[StrictStr]] = Field(default=None, description="Dropdown: required; the choices the signer picks from, at least one (up to 100 characters each).")
|
|
52
|
+
size: Optional[StrictInt] = Field(default=None, description="Checkbox and Radio: the side of the tick or dot in points, centred in the box. Defaults to the box's smaller side.")
|
|
53
|
+
tool_tip: Optional[StrictStr] = Field(default=None, description="Shown when the signer points at the field. Defaults to the label.")
|
|
54
|
+
width: Union[StrictFloat, StrictInt] = Field(description="In PDF points (1/72 inch): the field's width (each option's, for Radio). The field must fit on its page.")
|
|
55
|
+
x: Optional[Union[StrictFloat, StrictInt]] = Field(default=None, description="In PDF points (1/72 inch): from the page's left edge to the field's left edge. Required for every type but Radio (whose options have their own).")
|
|
56
|
+
y: Optional[Union[StrictFloat, StrictInt]] = Field(default=None, description="In PDF points (1/72 inch): from the page's top edge to the field's top edge. Required for every type but Radio.")
|
|
57
|
+
__properties: ClassVar[List[str]] = ["align", "checked", "date_format", "document_id", "font", "font_size", "formula", "group_name", "height", "is_multi_line", "is_required", "items", "kind", "label", "max_length", "number_format", "page", "page_number", "placeholder", "readonly", "select_options", "size", "tool_tip", "width", "x", "y"]
|
|
58
|
+
|
|
59
|
+
@field_validator('font')
|
|
60
|
+
def font_validate_enum(cls, value):
|
|
61
|
+
"""Validates the enum"""
|
|
62
|
+
if value is None:
|
|
63
|
+
return value
|
|
64
|
+
|
|
65
|
+
if value not in set(['Roboto', 'Open Sans', 'Lato', 'Inter']):
|
|
66
|
+
raise ValueError("must be one of enum values ('Roboto', 'Open Sans', 'Lato', 'Inter')")
|
|
67
|
+
return value
|
|
68
|
+
|
|
69
|
+
model_config = ConfigDict(
|
|
70
|
+
populate_by_name=True,
|
|
71
|
+
validate_assignment=True,
|
|
72
|
+
protected_namespaces=(),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def to_str(self) -> str:
|
|
77
|
+
"""Returns the string representation of the model using alias"""
|
|
78
|
+
return pprint.pformat(self.model_dump(by_alias=True))
|
|
79
|
+
|
|
80
|
+
def to_json(self) -> str:
|
|
81
|
+
"""Returns the JSON representation of the model using alias"""
|
|
82
|
+
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
|
|
83
|
+
return json.dumps(self.to_dict())
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def from_json(cls, json_str: str) -> Optional[Self]:
|
|
87
|
+
"""Create an instance of CreateTemplateFieldsRequestInner from a JSON string"""
|
|
88
|
+
return cls.from_dict(json.loads(json_str))
|
|
89
|
+
|
|
90
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
91
|
+
"""Return the dictionary representation of the model using alias.
|
|
92
|
+
|
|
93
|
+
This has the following differences from calling pydantic's
|
|
94
|
+
`self.model_dump(by_alias=True)`:
|
|
95
|
+
|
|
96
|
+
* `None` is only added to the output dict for nullable fields that
|
|
97
|
+
were set at model initialization. Other fields with value `None`
|
|
98
|
+
are ignored.
|
|
99
|
+
"""
|
|
100
|
+
excluded_fields: Set[str] = set([
|
|
101
|
+
])
|
|
102
|
+
|
|
103
|
+
_dict = self.model_dump(
|
|
104
|
+
by_alias=True,
|
|
105
|
+
exclude=excluded_fields,
|
|
106
|
+
exclude_none=True,
|
|
107
|
+
)
|
|
108
|
+
# override the default output from pydantic by calling `to_dict()` of each item in items (list)
|
|
109
|
+
_items = []
|
|
110
|
+
if self.items:
|
|
111
|
+
for _item_items in self.items:
|
|
112
|
+
if _item_items:
|
|
113
|
+
_items.append(_item_items.to_dict())
|
|
114
|
+
_dict['items'] = _items
|
|
115
|
+
return _dict
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
|
|
119
|
+
"""Create an instance of CreateTemplateFieldsRequestInner from a dict"""
|
|
120
|
+
if obj is None:
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
if not isinstance(obj, dict):
|
|
124
|
+
return cls.model_validate(obj)
|
|
125
|
+
|
|
126
|
+
_obj = cls.model_validate({
|
|
127
|
+
"align": obj.get("align"),
|
|
128
|
+
"checked": obj.get("checked") if obj.get("checked") is not None else False,
|
|
129
|
+
"date_format": obj.get("date_format"),
|
|
130
|
+
"document_id": obj.get("document_id"),
|
|
131
|
+
"font": obj.get("font") if obj.get("font") is not None else 'Roboto',
|
|
132
|
+
"font_size": obj.get("font_size"),
|
|
133
|
+
"formula": obj.get("formula"),
|
|
134
|
+
"group_name": obj.get("group_name"),
|
|
135
|
+
"height": obj.get("height"),
|
|
136
|
+
"is_multi_line": obj.get("is_multi_line") if obj.get("is_multi_line") is not None else False,
|
|
137
|
+
"is_required": obj.get("is_required") if obj.get("is_required") is not None else True,
|
|
138
|
+
"items": [UpdateDocumentFieldByBodyRequestItemsInner.from_dict(_item) for _item in obj["items"]] if obj.get("items") is not None else None,
|
|
139
|
+
"kind": obj.get("kind"),
|
|
140
|
+
"label": obj.get("label"),
|
|
141
|
+
"max_length": obj.get("max_length"),
|
|
142
|
+
"number_format": obj.get("number_format"),
|
|
143
|
+
"page": obj.get("page"),
|
|
144
|
+
"page_number": obj.get("page_number"),
|
|
145
|
+
"placeholder": obj.get("placeholder"),
|
|
146
|
+
"readonly": obj.get("readonly") if obj.get("readonly") is not None else False,
|
|
147
|
+
"select_options": obj.get("select_options"),
|
|
148
|
+
"size": obj.get("size"),
|
|
149
|
+
"tool_tip": obj.get("tool_tip"),
|
|
150
|
+
"width": obj.get("width"),
|
|
151
|
+
"x": obj.get("x"),
|
|
152
|
+
"y": obj.get("y")
|
|
153
|
+
})
|
|
154
|
+
return _obj
|
|
155
|
+
|
|
156
|
+
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Signedly API
|
|
5
|
+
|
|
6
|
+
The Signedly REST API lets your system upload documents, add signatories and fields, send them for signing and download the signed result. Every path starts with `/ap1/`, and every request and response body is JSON unless an endpoint says otherwise (the uploads are `multipart/form-data`). ## Authentication 1. In the Signedly web app, open **Account & Settings → API** and generate a **client ID** and **client secret** for your organisation. 2. `POST /ap1/token/` with them. You get an `access_token` (valid **2 hours**) and a `refresh_token` (valid **7 days**). While both are still valid, calling `/ap1/token/` again returns the same pair. 3. Send the access token on every other call: `Authorization: Bearer <access_token>`. 4. When the access token expires, `POST /ap1/refreshtoken/` with the refresh token for a new access token. When the refresh token has expired too, call `/ap1/token/` again. `/ap1/token/` and `/ap1/refreshtoken/` allow 60 calls a minute per IP address; more answer 429. A missing, unknown or expired access token answers 403 (`\"Client not found\"` or `\"Access token expired\"`). The API acts for your **organisation's admin account**: documents you upload belong to it and appear in its Signedly web app, and you can reach only that account's documents, templates, signatories and fields. Another organisation's IDs answer 404, as if they didn't exist. ## Plans What the API allows depends on the Signedly plan of your **organisation's admin account** (Account & Settings → Account & Plan). The same rules apply to live and test keys. | What | Free | Personal Pro | Business Pro | Teams | |---|---|---|---|---| | Generate live and test keys (Account & Settings → API) | – | ✓ | ✓ | ✓ | | Every endpoint, `/ap1/token/` and `/ap1/refreshtoken/` included | – | ✓ | ✓ | ✓ | | `require_email_otp` on `POST /ap1/signatories/` and `POST /ap1/signatories/bulk/` | – | ✓ | ✓ | ✓ | | Bulk send: `GET /ap1/bulk-sends/columns/` and `POST /ap1/bulk-sends/` | – | – | – | ✓ | The API is part of the paid plans. On Free (or with no plan) every call answers 403 `\"... Your plan doesn't include API access.\"`, with live or test keys, also with a token or keys made earlier; they work again once the plan is paid. On a paid plan, a feature it doesn't include answers 400 with `\"Your plan doesn't include ...\"` and changes nothing, and `GET /ap1/bulk-sends/{id}/` still reports bulk sends already made. Live documents also need an API contract with Signedly. Without one, or with an unpaid API invoice, `POST /ap1/upload/` (except templates), `/ap1/upload_app/`, `/ap1/documents/fromtemplate/` and `/ap1/bulk-sends/` answer 402 with live keys. Test keys work without a contract (see Sandbox). ## Sending a document for signing 1. `POST /ap1/upload/` a PDF or DOCX (`type` 2). The document is prepared in the background: find it in `GET /ap1/documents/?name=<name>` (its `upload.id` is the upload's `id`), and wait until `GET /ap1/documents/{id}/pages/` lists its pages. 2. `POST /ap1/signatories/` (one) or `POST /ap1/signatories/bulk/` (several) to add the people who sign. 3. `POST /ap1/documents/fields/` to place the fields each signatory fills in. 4. Send it: - `POST /ap1/document/send/` emails every signatory a signing link (with ordered signing, only the first; each next one is emailed in turn), or - `POST /ap1/document/send_url/` sends no email and returns the first signatory's signing link, for you to show or send yourself. 5. Poll `GET /ap1/documents-status-list/` (or `GET /ap1/documents/`) until the status is 6 (Completed), then `GET /ap1/document/download-link/` for the signed PDF and `GET /ap1/document/download-audit-link/` for its audit trail. **From a template:** upload with `type` 4 and a `template_name` (or make the template in the web app), place its fields with `POST /ap1/templates/fields/`, then for each signing make a document with `POST /ap1/documents/fromtemplate/`, add its signatory, and send it. **One signer, one call:** `POST /ap1/upload_app/` uploads a PDF or DOCX for one signer and returns their signing link in `redirect_url`; the signer places their own signature. After signing they go to the `redirect_url` you sent, and the signed PDF and its audit trail are emailed to `final_email` (otherwise to your organisation's admin). ## Quick start with curl Each step uses an ID from the step before. `$API` is this server, e.g. `https://l0ads.signedly.com`. ```bash # 1. Token curl -X POST $API/ap1/token/ -H 'Content-Type: application/json' \\ -d '{\"client_id\": \"<client id>\", \"client_secret\": \"<client secret>\"}' TOKEN=<access_token from the answer> # 2. Upload a PDF (answers with the upload's id) curl -X POST $API/ap1/upload/ -H \"Authorization: Bearer $TOKEN\" \\ -F file=@nda.pdf -F name=nda-ann-lee -F type=2 # 3. Find the document, then wait until it has pages curl \"$API/ap1/documents/?name=nda-ann-lee\" -H \"Authorization: Bearer $TOKEN\" DOC=<results[0].id> curl $API/ap1/documents/$DOC/pages/ -H \"Authorization: Bearer $TOKEN\" PAGE=<[0].id> # 4. Add the signatory curl -X POST $API/ap1/signatories/ -H \"Authorization: Bearer $TOKEN\" \\ -H 'Content-Type: application/json' \\ -d '{\"document_id\": \"'$DOC'\", \"name\": \"Ann Lee\", \"email\": \"ann@example.com\"}' SIGNER=<id from the answer> # 5. Place her signature: 150 × 40 points, 72 points from the left, 640 from the top curl -X POST $API/ap1/documents/fields/ -H \"Authorization: Bearer $TOKEN\" \\ -H 'Content-Type: application/json' \\ -d '[{\"document_id\": \"'$DOC'\", \"signatory_id\": \"'$SIGNER'\", \"kind\": 2, \"label\": \"Signature\", \"placeholder\": \"Sign here\", \"page\": \"'$PAGE'\", \"page_number\": 0, \"x\": 72, \"y\": 640, \"width\": 150, \"height\": 40}]' # 6. Send it (Ann gets the signing email) curl -X POST $API/ap1/document/send/ -H \"Authorization: Bearer $TOKEN\" \\ -H 'Content-Type: application/json' \\ -d '{\"document_id\": \"'$DOC'\", \"subject\": \"Please sign the NDA\", \"message\": \"Hi Ann, please sign by Friday.\", \"expiry_date\": \"2026-12-31\"}' # 7. Poll until status is 6 (Completed), then get the signed PDF curl \"$API/ap1/documents-status-list/?document_ids=$DOC\" -H \"Authorization: Bearer $TOKEN\" curl \"$API/ap1/document/download-link/?id=$DOC\" -H \"Authorization: Bearer $TOKEN\" ``` ## Postman Import the whole API into Postman with **Import → Link** and `<this server>/swaggerap1/postman.json`. Set `client_id` and `client_secret` in the collection's **Variables**, run **Get an access token** (it saves the token for every other request), and set `document_id` when a request needs one. ## Webhooks Instead of polling, add an endpoint with `POST /ap1/webhooks/` (or in **Account & Settings → API**) and Signedly posts your organisation's document events to it: `document.sent`, `document.viewed`, `document.signed`, `document.approved`, `document.completed`, `document.rejected` and `document.expired` (all of them, or the `events` you choose). Each is a JSON `POST`: ```json {\"id\": \"5b7f…\", \"type\": \"document.signed\", \"created_at\": \"2026-10-27T09:12:00+00:00\", \"test\": false, \"data\": {\"document\": {\"id\": \"6f0c…\", \"name\": \"nda.pdf\", \"status\": 5}, \"recipient\": {\"id\": \"0b6e…\", \"email\": \"ann@example.com\", \"name\": \"Ann Lee\", \"role\": \"signer\"}}} ``` `recipient` is null for events no recipient caused (sent, completed, expired); `test` is true for documents made with your test keys. Fetch anything else, such as download links, with your access token: events never carry links. **Check every event is from Signedly.** The `Signedly-Signature` header is `t=<unix seconds>,v1=<signature>`, where the signature is the hex HMAC-SHA256 of `<t>.<raw body>` with the endpoint's secret. Refuse an event whose signature doesn't match or whose `t` is more than 5 minutes off: ```python import hashlib, hmac, time def is_from_signedly(secret, header, raw_body): parts = dict(item.split(\"=\", 1) for item in header.split(\",\")) signed = f\"{parts['t']}.{raw_body.decode()}\".encode() expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() fresh = abs(time.time() - int(parts[\"t\"])) <= 300 return fresh and hmac.compare_digest(parts[\"v1\"], expected) ``` **Answer 2xx within 10 seconds**, then do the work. Anything else is retried after 1 and 5 minutes, then 30 minutes and 2, 5 and 10 hours (7 tries in about 18 hours). `Signedly-Event-Id` is the same on every try of an event: use it to ignore repeats. After 20 events in a row fail for good, Signedly switches the endpoint off and emails your organisation's admin; switch it on again with `PUT /ap1/webhooks/{id}/` and `\"is_active\": true`. Each endpoint's deliveries and their last answers are at `GET /ap1/webhooks/{id}/deliveries/`, and any can be sent again. ## Sandbox Build and test against the API without paying for documents: in **Account & Settings → API**, generate your **test keys**, and get a token with them at `/ap1/token/` as usual. Everything works the same, except that what you upload or make from a template with a test token is a **test document** (`is_test` on its upload): - every page of its PDF and of its audit trail says **TEST – NOT LEGALLY BINDING**; - its emails are real, with `[TEST]` at the start of the subject; - it isn't billed or counted toward your plan; - your organisation can make 50 a calendar month (templates don't count); the next upload answers 429. ## Fields Positions and sizes are in **PDF points** (1/72 inch), measured from the **top-left corner** of the page as it is displayed (rotation applied). `page_number` counts pages from 0. A field must sit wholly on its page (1 point of tolerance); otherwise it is refused with 400. A field's page and signatory can't be changed later: delete the field and add it again. ## Status values **Document `status`:** 1 Draft · 2 Signees Added · 3 Prepared · 4 Sent · 5 In Progress · 6 Completed · 7 Rejected · 8 Archived · 9 Expired. Signatories, fields and sends are only accepted while a document is 1–3 (not yet sent). **Signatory `status`:** 1 Waiting · 2 Invited · 3 Signed · 4 Viewed · 5 Rejected. ## Lists `GET /ap1/documents/`, `/ap1/documents-status-list/`, `/ap1/templates/`, `/ap1/logs/` and `/ap1/logs/hits/` are paged: pass `limit` (default 10) and `offset`, and read `count`, `next`, `previous` and `results` from the response. ## Errors | Status | Meaning | |---|---| | 400 | The request was refused, also when your plan doesn't include it (see Plans); the body says why (`{\"error\": \"...\"}`, or the name of each bad field with its messages). Nothing was changed. | | 403 | The access token is missing, unknown or expired; the client ID and secret or the refresh token were refused; the organisation's plan has no API access (Free, see Plans); or an `expiry_date` isn't in the future (`\"Invalid Date Entered\"`). | | 402 | No API contract, or an unpaid API invoice: live documents are refused (test keys keep working). | | 404 | The document, template, signatory or field doesn't exist or isn't yours. | | 429 | Too many token requests (wait a minute), or this month's 50 test documents are used. | | 5xx | Our side failed; retry later. | A send that times out may still have worked: check the document's status before sending again (a second send of a sent document answers 400). Download, view and audit links are valid for about 7 days; ask for a new one whenever you need it.
|
|
7
|
+
|
|
8
|
+
The version of the OpenAPI document: v1
|
|
9
|
+
Contact: info@signedly.com
|
|
10
|
+
Generated by OpenAPI Generator (https://openapi-generator.tech)
|
|
11
|
+
|
|
12
|
+
Do not edit the class manually.
|
|
13
|
+
""" # noqa: E501
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
import pprint
|
|
18
|
+
import re # noqa: F401
|
|
19
|
+
import json
|
|
20
|
+
|
|
21
|
+
from pydantic import BaseModel, ConfigDict, Field, StrictStr, field_validator
|
|
22
|
+
from typing import Any, ClassVar, Dict, List, Optional
|
|
23
|
+
from typing import Optional, Set
|
|
24
|
+
from typing_extensions import Self
|
|
25
|
+
|
|
26
|
+
class CreateWebhookEndpointRequest(BaseModel):
|
|
27
|
+
"""
|
|
28
|
+
CreateWebhookEndpointRequest
|
|
29
|
+
""" # noqa: E501
|
|
30
|
+
description: Optional[StrictStr] = Field(default=None, description="Your note about it (up to 200 characters).")
|
|
31
|
+
events: Optional[List[StrictStr]] = Field(default=None, description="The event types to send; leave out for all: `document.approved`, `document.completed`, `document.expired`, `document.rejected`, `document.sent`, `document.signed`, `document.viewed`.")
|
|
32
|
+
url: StrictStr = Field(description="Where to post events: https://, on a public internet address. Redirects aren't followed.")
|
|
33
|
+
__properties: ClassVar[List[str]] = ["description", "events", "url"]
|
|
34
|
+
|
|
35
|
+
@field_validator('events')
|
|
36
|
+
def events_validate_enum(cls, value):
|
|
37
|
+
"""Validates the enum"""
|
|
38
|
+
if value is None:
|
|
39
|
+
return value
|
|
40
|
+
|
|
41
|
+
for i in value:
|
|
42
|
+
if i not in set(['document.approved', 'document.completed', 'document.expired', 'document.rejected', 'document.sent', 'document.signed', 'document.viewed']):
|
|
43
|
+
raise ValueError("each list item must be one of ('document.approved', 'document.completed', 'document.expired', 'document.rejected', 'document.sent', 'document.signed', 'document.viewed')")
|
|
44
|
+
return value
|
|
45
|
+
|
|
46
|
+
model_config = ConfigDict(
|
|
47
|
+
populate_by_name=True,
|
|
48
|
+
validate_assignment=True,
|
|
49
|
+
protected_namespaces=(),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def to_str(self) -> str:
|
|
54
|
+
"""Returns the string representation of the model using alias"""
|
|
55
|
+
return pprint.pformat(self.model_dump(by_alias=True))
|
|
56
|
+
|
|
57
|
+
def to_json(self) -> str:
|
|
58
|
+
"""Returns the JSON representation of the model using alias"""
|
|
59
|
+
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
|
|
60
|
+
return json.dumps(self.to_dict())
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def from_json(cls, json_str: str) -> Optional[Self]:
|
|
64
|
+
"""Create an instance of CreateWebhookEndpointRequest from a JSON string"""
|
|
65
|
+
return cls.from_dict(json.loads(json_str))
|
|
66
|
+
|
|
67
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
68
|
+
"""Return the dictionary representation of the model using alias.
|
|
69
|
+
|
|
70
|
+
This has the following differences from calling pydantic's
|
|
71
|
+
`self.model_dump(by_alias=True)`:
|
|
72
|
+
|
|
73
|
+
* `None` is only added to the output dict for nullable fields that
|
|
74
|
+
were set at model initialization. Other fields with value `None`
|
|
75
|
+
are ignored.
|
|
76
|
+
"""
|
|
77
|
+
excluded_fields: Set[str] = set([
|
|
78
|
+
])
|
|
79
|
+
|
|
80
|
+
_dict = self.model_dump(
|
|
81
|
+
by_alias=True,
|
|
82
|
+
exclude=excluded_fields,
|
|
83
|
+
exclude_none=True,
|
|
84
|
+
)
|
|
85
|
+
return _dict
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
|
|
89
|
+
"""Create an instance of CreateWebhookEndpointRequest from a dict"""
|
|
90
|
+
if obj is None:
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
if not isinstance(obj, dict):
|
|
94
|
+
return cls.model_validate(obj)
|
|
95
|
+
|
|
96
|
+
_obj = cls.model_validate({
|
|
97
|
+
"description": obj.get("description"),
|
|
98
|
+
"events": obj.get("events"),
|
|
99
|
+
"url": obj.get("url")
|
|
100
|
+
})
|
|
101
|
+
return _obj
|
|
102
|
+
|
|
103
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Signedly API
|
|
5
|
+
|
|
6
|
+
The Signedly REST API lets your system upload documents, add signatories and fields, send them for signing and download the signed result. Every path starts with `/ap1/`, and every request and response body is JSON unless an endpoint says otherwise (the uploads are `multipart/form-data`). ## Authentication 1. In the Signedly web app, open **Account & Settings → API** and generate a **client ID** and **client secret** for your organisation. 2. `POST /ap1/token/` with them. You get an `access_token` (valid **2 hours**) and a `refresh_token` (valid **7 days**). While both are still valid, calling `/ap1/token/` again returns the same pair. 3. Send the access token on every other call: `Authorization: Bearer <access_token>`. 4. When the access token expires, `POST /ap1/refreshtoken/` with the refresh token for a new access token. When the refresh token has expired too, call `/ap1/token/` again. `/ap1/token/` and `/ap1/refreshtoken/` allow 60 calls a minute per IP address; more answer 429. A missing, unknown or expired access token answers 403 (`\"Client not found\"` or `\"Access token expired\"`). The API acts for your **organisation's admin account**: documents you upload belong to it and appear in its Signedly web app, and you can reach only that account's documents, templates, signatories and fields. Another organisation's IDs answer 404, as if they didn't exist. ## Plans What the API allows depends on the Signedly plan of your **organisation's admin account** (Account & Settings → Account & Plan). The same rules apply to live and test keys. | What | Free | Personal Pro | Business Pro | Teams | |---|---|---|---|---| | Generate live and test keys (Account & Settings → API) | – | ✓ | ✓ | ✓ | | Every endpoint, `/ap1/token/` and `/ap1/refreshtoken/` included | – | ✓ | ✓ | ✓ | | `require_email_otp` on `POST /ap1/signatories/` and `POST /ap1/signatories/bulk/` | – | ✓ | ✓ | ✓ | | Bulk send: `GET /ap1/bulk-sends/columns/` and `POST /ap1/bulk-sends/` | – | – | – | ✓ | The API is part of the paid plans. On Free (or with no plan) every call answers 403 `\"... Your plan doesn't include API access.\"`, with live or test keys, also with a token or keys made earlier; they work again once the plan is paid. On a paid plan, a feature it doesn't include answers 400 with `\"Your plan doesn't include ...\"` and changes nothing, and `GET /ap1/bulk-sends/{id}/` still reports bulk sends already made. Live documents also need an API contract with Signedly. Without one, or with an unpaid API invoice, `POST /ap1/upload/` (except templates), `/ap1/upload_app/`, `/ap1/documents/fromtemplate/` and `/ap1/bulk-sends/` answer 402 with live keys. Test keys work without a contract (see Sandbox). ## Sending a document for signing 1. `POST /ap1/upload/` a PDF or DOCX (`type` 2). The document is prepared in the background: find it in `GET /ap1/documents/?name=<name>` (its `upload.id` is the upload's `id`), and wait until `GET /ap1/documents/{id}/pages/` lists its pages. 2. `POST /ap1/signatories/` (one) or `POST /ap1/signatories/bulk/` (several) to add the people who sign. 3. `POST /ap1/documents/fields/` to place the fields each signatory fills in. 4. Send it: - `POST /ap1/document/send/` emails every signatory a signing link (with ordered signing, only the first; each next one is emailed in turn), or - `POST /ap1/document/send_url/` sends no email and returns the first signatory's signing link, for you to show or send yourself. 5. Poll `GET /ap1/documents-status-list/` (or `GET /ap1/documents/`) until the status is 6 (Completed), then `GET /ap1/document/download-link/` for the signed PDF and `GET /ap1/document/download-audit-link/` for its audit trail. **From a template:** upload with `type` 4 and a `template_name` (or make the template in the web app), place its fields with `POST /ap1/templates/fields/`, then for each signing make a document with `POST /ap1/documents/fromtemplate/`, add its signatory, and send it. **One signer, one call:** `POST /ap1/upload_app/` uploads a PDF or DOCX for one signer and returns their signing link in `redirect_url`; the signer places their own signature. After signing they go to the `redirect_url` you sent, and the signed PDF and its audit trail are emailed to `final_email` (otherwise to your organisation's admin). ## Quick start with curl Each step uses an ID from the step before. `$API` is this server, e.g. `https://l0ads.signedly.com`. ```bash # 1. Token curl -X POST $API/ap1/token/ -H 'Content-Type: application/json' \\ -d '{\"client_id\": \"<client id>\", \"client_secret\": \"<client secret>\"}' TOKEN=<access_token from the answer> # 2. Upload a PDF (answers with the upload's id) curl -X POST $API/ap1/upload/ -H \"Authorization: Bearer $TOKEN\" \\ -F file=@nda.pdf -F name=nda-ann-lee -F type=2 # 3. Find the document, then wait until it has pages curl \"$API/ap1/documents/?name=nda-ann-lee\" -H \"Authorization: Bearer $TOKEN\" DOC=<results[0].id> curl $API/ap1/documents/$DOC/pages/ -H \"Authorization: Bearer $TOKEN\" PAGE=<[0].id> # 4. Add the signatory curl -X POST $API/ap1/signatories/ -H \"Authorization: Bearer $TOKEN\" \\ -H 'Content-Type: application/json' \\ -d '{\"document_id\": \"'$DOC'\", \"name\": \"Ann Lee\", \"email\": \"ann@example.com\"}' SIGNER=<id from the answer> # 5. Place her signature: 150 × 40 points, 72 points from the left, 640 from the top curl -X POST $API/ap1/documents/fields/ -H \"Authorization: Bearer $TOKEN\" \\ -H 'Content-Type: application/json' \\ -d '[{\"document_id\": \"'$DOC'\", \"signatory_id\": \"'$SIGNER'\", \"kind\": 2, \"label\": \"Signature\", \"placeholder\": \"Sign here\", \"page\": \"'$PAGE'\", \"page_number\": 0, \"x\": 72, \"y\": 640, \"width\": 150, \"height\": 40}]' # 6. Send it (Ann gets the signing email) curl -X POST $API/ap1/document/send/ -H \"Authorization: Bearer $TOKEN\" \\ -H 'Content-Type: application/json' \\ -d '{\"document_id\": \"'$DOC'\", \"subject\": \"Please sign the NDA\", \"message\": \"Hi Ann, please sign by Friday.\", \"expiry_date\": \"2026-12-31\"}' # 7. Poll until status is 6 (Completed), then get the signed PDF curl \"$API/ap1/documents-status-list/?document_ids=$DOC\" -H \"Authorization: Bearer $TOKEN\" curl \"$API/ap1/document/download-link/?id=$DOC\" -H \"Authorization: Bearer $TOKEN\" ``` ## Postman Import the whole API into Postman with **Import → Link** and `<this server>/swaggerap1/postman.json`. Set `client_id` and `client_secret` in the collection's **Variables**, run **Get an access token** (it saves the token for every other request), and set `document_id` when a request needs one. ## Webhooks Instead of polling, add an endpoint with `POST /ap1/webhooks/` (or in **Account & Settings → API**) and Signedly posts your organisation's document events to it: `document.sent`, `document.viewed`, `document.signed`, `document.approved`, `document.completed`, `document.rejected` and `document.expired` (all of them, or the `events` you choose). Each is a JSON `POST`: ```json {\"id\": \"5b7f…\", \"type\": \"document.signed\", \"created_at\": \"2026-10-27T09:12:00+00:00\", \"test\": false, \"data\": {\"document\": {\"id\": \"6f0c…\", \"name\": \"nda.pdf\", \"status\": 5}, \"recipient\": {\"id\": \"0b6e…\", \"email\": \"ann@example.com\", \"name\": \"Ann Lee\", \"role\": \"signer\"}}} ``` `recipient` is null for events no recipient caused (sent, completed, expired); `test` is true for documents made with your test keys. Fetch anything else, such as download links, with your access token: events never carry links. **Check every event is from Signedly.** The `Signedly-Signature` header is `t=<unix seconds>,v1=<signature>`, where the signature is the hex HMAC-SHA256 of `<t>.<raw body>` with the endpoint's secret. Refuse an event whose signature doesn't match or whose `t` is more than 5 minutes off: ```python import hashlib, hmac, time def is_from_signedly(secret, header, raw_body): parts = dict(item.split(\"=\", 1) for item in header.split(\",\")) signed = f\"{parts['t']}.{raw_body.decode()}\".encode() expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() fresh = abs(time.time() - int(parts[\"t\"])) <= 300 return fresh and hmac.compare_digest(parts[\"v1\"], expected) ``` **Answer 2xx within 10 seconds**, then do the work. Anything else is retried after 1 and 5 minutes, then 30 minutes and 2, 5 and 10 hours (7 tries in about 18 hours). `Signedly-Event-Id` is the same on every try of an event: use it to ignore repeats. After 20 events in a row fail for good, Signedly switches the endpoint off and emails your organisation's admin; switch it on again with `PUT /ap1/webhooks/{id}/` and `\"is_active\": true`. Each endpoint's deliveries and their last answers are at `GET /ap1/webhooks/{id}/deliveries/`, and any can be sent again. ## Sandbox Build and test against the API without paying for documents: in **Account & Settings → API**, generate your **test keys**, and get a token with them at `/ap1/token/` as usual. Everything works the same, except that what you upload or make from a template with a test token is a **test document** (`is_test` on its upload): - every page of its PDF and of its audit trail says **TEST – NOT LEGALLY BINDING**; - its emails are real, with `[TEST]` at the start of the subject; - it isn't billed or counted toward your plan; - your organisation can make 50 a calendar month (templates don't count); the next upload answers 429. ## Fields Positions and sizes are in **PDF points** (1/72 inch), measured from the **top-left corner** of the page as it is displayed (rotation applied). `page_number` counts pages from 0. A field must sit wholly on its page (1 point of tolerance); otherwise it is refused with 400. A field's page and signatory can't be changed later: delete the field and add it again. ## Status values **Document `status`:** 1 Draft · 2 Signees Added · 3 Prepared · 4 Sent · 5 In Progress · 6 Completed · 7 Rejected · 8 Archived · 9 Expired. Signatories, fields and sends are only accepted while a document is 1–3 (not yet sent). **Signatory `status`:** 1 Waiting · 2 Invited · 3 Signed · 4 Viewed · 5 Rejected. ## Lists `GET /ap1/documents/`, `/ap1/documents-status-list/`, `/ap1/templates/`, `/ap1/logs/` and `/ap1/logs/hits/` are paged: pass `limit` (default 10) and `offset`, and read `count`, `next`, `previous` and `results` from the response. ## Errors | Status | Meaning | |---|---| | 400 | The request was refused, also when your plan doesn't include it (see Plans); the body says why (`{\"error\": \"...\"}`, or the name of each bad field with its messages). Nothing was changed. | | 403 | The access token is missing, unknown or expired; the client ID and secret or the refresh token were refused; the organisation's plan has no API access (Free, see Plans); or an `expiry_date` isn't in the future (`\"Invalid Date Entered\"`). | | 402 | No API contract, or an unpaid API invoice: live documents are refused (test keys keep working). | | 404 | The document, template, signatory or field doesn't exist or isn't yours. | | 429 | Too many token requests (wait a minute), or this month's 50 test documents are used. | | 5xx | Our side failed; retry later. | A send that times out may still have worked: check the document's status before sending again (a second send of a sent document answers 400). Download, view and audit links are valid for about 7 days; ask for a new one whenever you need it.
|
|
7
|
+
|
|
8
|
+
The version of the OpenAPI document: v1
|
|
9
|
+
Contact: info@signedly.com
|
|
10
|
+
Generated by OpenAPI Generator (https://openapi-generator.tech)
|
|
11
|
+
|
|
12
|
+
Do not edit the class manually.
|
|
13
|
+
""" # noqa: E501
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
import pprint
|
|
18
|
+
import re # noqa: F401
|
|
19
|
+
import json
|
|
20
|
+
|
|
21
|
+
from pydantic import BaseModel, ConfigDict, Field, StrictStr
|
|
22
|
+
from typing import Any, ClassVar, Dict, List
|
|
23
|
+
from typing import Optional, Set
|
|
24
|
+
from typing_extensions import Self
|
|
25
|
+
|
|
26
|
+
class DeleteFieldRequest(BaseModel):
|
|
27
|
+
"""
|
|
28
|
+
DeleteFieldRequest
|
|
29
|
+
""" # noqa: E501
|
|
30
|
+
document_id: StrictStr = Field(description="The ID of the document (or template) the item is on.")
|
|
31
|
+
__properties: ClassVar[List[str]] = ["document_id"]
|
|
32
|
+
|
|
33
|
+
model_config = ConfigDict(
|
|
34
|
+
populate_by_name=True,
|
|
35
|
+
validate_assignment=True,
|
|
36
|
+
protected_namespaces=(),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def to_str(self) -> str:
|
|
41
|
+
"""Returns the string representation of the model using alias"""
|
|
42
|
+
return pprint.pformat(self.model_dump(by_alias=True))
|
|
43
|
+
|
|
44
|
+
def to_json(self) -> str:
|
|
45
|
+
"""Returns the JSON representation of the model using alias"""
|
|
46
|
+
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
|
|
47
|
+
return json.dumps(self.to_dict())
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def from_json(cls, json_str: str) -> Optional[Self]:
|
|
51
|
+
"""Create an instance of DeleteFieldRequest from a JSON string"""
|
|
52
|
+
return cls.from_dict(json.loads(json_str))
|
|
53
|
+
|
|
54
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
55
|
+
"""Return the dictionary representation of the model using alias.
|
|
56
|
+
|
|
57
|
+
This has the following differences from calling pydantic's
|
|
58
|
+
`self.model_dump(by_alias=True)`:
|
|
59
|
+
|
|
60
|
+
* `None` is only added to the output dict for nullable fields that
|
|
61
|
+
were set at model initialization. Other fields with value `None`
|
|
62
|
+
are ignored.
|
|
63
|
+
"""
|
|
64
|
+
excluded_fields: Set[str] = set([
|
|
65
|
+
])
|
|
66
|
+
|
|
67
|
+
_dict = self.model_dump(
|
|
68
|
+
by_alias=True,
|
|
69
|
+
exclude=excluded_fields,
|
|
70
|
+
exclude_none=True,
|
|
71
|
+
)
|
|
72
|
+
return _dict
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
|
|
76
|
+
"""Create an instance of DeleteFieldRequest from a dict"""
|
|
77
|
+
if obj is None:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
if not isinstance(obj, dict):
|
|
81
|
+
return cls.model_validate(obj)
|
|
82
|
+
|
|
83
|
+
_obj = cls.model_validate({
|
|
84
|
+
"document_id": obj.get("document_id")
|
|
85
|
+
})
|
|
86
|
+
return _obj
|
|
87
|
+
|
|
88
|
+
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Signedly API
|
|
5
|
+
|
|
6
|
+
The Signedly REST API lets your system upload documents, add signatories and fields, send them for signing and download the signed result. Every path starts with `/ap1/`, and every request and response body is JSON unless an endpoint says otherwise (the uploads are `multipart/form-data`). ## Authentication 1. In the Signedly web app, open **Account & Settings → API** and generate a **client ID** and **client secret** for your organisation. 2. `POST /ap1/token/` with them. You get an `access_token` (valid **2 hours**) and a `refresh_token` (valid **7 days**). While both are still valid, calling `/ap1/token/` again returns the same pair. 3. Send the access token on every other call: `Authorization: Bearer <access_token>`. 4. When the access token expires, `POST /ap1/refreshtoken/` with the refresh token for a new access token. When the refresh token has expired too, call `/ap1/token/` again. `/ap1/token/` and `/ap1/refreshtoken/` allow 60 calls a minute per IP address; more answer 429. A missing, unknown or expired access token answers 403 (`\"Client not found\"` or `\"Access token expired\"`). The API acts for your **organisation's admin account**: documents you upload belong to it and appear in its Signedly web app, and you can reach only that account's documents, templates, signatories and fields. Another organisation's IDs answer 404, as if they didn't exist. ## Plans What the API allows depends on the Signedly plan of your **organisation's admin account** (Account & Settings → Account & Plan). The same rules apply to live and test keys. | What | Free | Personal Pro | Business Pro | Teams | |---|---|---|---|---| | Generate live and test keys (Account & Settings → API) | – | ✓ | ✓ | ✓ | | Every endpoint, `/ap1/token/` and `/ap1/refreshtoken/` included | – | ✓ | ✓ | ✓ | | `require_email_otp` on `POST /ap1/signatories/` and `POST /ap1/signatories/bulk/` | – | ✓ | ✓ | ✓ | | Bulk send: `GET /ap1/bulk-sends/columns/` and `POST /ap1/bulk-sends/` | – | – | – | ✓ | The API is part of the paid plans. On Free (or with no plan) every call answers 403 `\"... Your plan doesn't include API access.\"`, with live or test keys, also with a token or keys made earlier; they work again once the plan is paid. On a paid plan, a feature it doesn't include answers 400 with `\"Your plan doesn't include ...\"` and changes nothing, and `GET /ap1/bulk-sends/{id}/` still reports bulk sends already made. Live documents also need an API contract with Signedly. Without one, or with an unpaid API invoice, `POST /ap1/upload/` (except templates), `/ap1/upload_app/`, `/ap1/documents/fromtemplate/` and `/ap1/bulk-sends/` answer 402 with live keys. Test keys work without a contract (see Sandbox). ## Sending a document for signing 1. `POST /ap1/upload/` a PDF or DOCX (`type` 2). The document is prepared in the background: find it in `GET /ap1/documents/?name=<name>` (its `upload.id` is the upload's `id`), and wait until `GET /ap1/documents/{id}/pages/` lists its pages. 2. `POST /ap1/signatories/` (one) or `POST /ap1/signatories/bulk/` (several) to add the people who sign. 3. `POST /ap1/documents/fields/` to place the fields each signatory fills in. 4. Send it: - `POST /ap1/document/send/` emails every signatory a signing link (with ordered signing, only the first; each next one is emailed in turn), or - `POST /ap1/document/send_url/` sends no email and returns the first signatory's signing link, for you to show or send yourself. 5. Poll `GET /ap1/documents-status-list/` (or `GET /ap1/documents/`) until the status is 6 (Completed), then `GET /ap1/document/download-link/` for the signed PDF and `GET /ap1/document/download-audit-link/` for its audit trail. **From a template:** upload with `type` 4 and a `template_name` (or make the template in the web app), place its fields with `POST /ap1/templates/fields/`, then for each signing make a document with `POST /ap1/documents/fromtemplate/`, add its signatory, and send it. **One signer, one call:** `POST /ap1/upload_app/` uploads a PDF or DOCX for one signer and returns their signing link in `redirect_url`; the signer places their own signature. After signing they go to the `redirect_url` you sent, and the signed PDF and its audit trail are emailed to `final_email` (otherwise to your organisation's admin). ## Quick start with curl Each step uses an ID from the step before. `$API` is this server, e.g. `https://l0ads.signedly.com`. ```bash # 1. Token curl -X POST $API/ap1/token/ -H 'Content-Type: application/json' \\ -d '{\"client_id\": \"<client id>\", \"client_secret\": \"<client secret>\"}' TOKEN=<access_token from the answer> # 2. Upload a PDF (answers with the upload's id) curl -X POST $API/ap1/upload/ -H \"Authorization: Bearer $TOKEN\" \\ -F file=@nda.pdf -F name=nda-ann-lee -F type=2 # 3. Find the document, then wait until it has pages curl \"$API/ap1/documents/?name=nda-ann-lee\" -H \"Authorization: Bearer $TOKEN\" DOC=<results[0].id> curl $API/ap1/documents/$DOC/pages/ -H \"Authorization: Bearer $TOKEN\" PAGE=<[0].id> # 4. Add the signatory curl -X POST $API/ap1/signatories/ -H \"Authorization: Bearer $TOKEN\" \\ -H 'Content-Type: application/json' \\ -d '{\"document_id\": \"'$DOC'\", \"name\": \"Ann Lee\", \"email\": \"ann@example.com\"}' SIGNER=<id from the answer> # 5. Place her signature: 150 × 40 points, 72 points from the left, 640 from the top curl -X POST $API/ap1/documents/fields/ -H \"Authorization: Bearer $TOKEN\" \\ -H 'Content-Type: application/json' \\ -d '[{\"document_id\": \"'$DOC'\", \"signatory_id\": \"'$SIGNER'\", \"kind\": 2, \"label\": \"Signature\", \"placeholder\": \"Sign here\", \"page\": \"'$PAGE'\", \"page_number\": 0, \"x\": 72, \"y\": 640, \"width\": 150, \"height\": 40}]' # 6. Send it (Ann gets the signing email) curl -X POST $API/ap1/document/send/ -H \"Authorization: Bearer $TOKEN\" \\ -H 'Content-Type: application/json' \\ -d '{\"document_id\": \"'$DOC'\", \"subject\": \"Please sign the NDA\", \"message\": \"Hi Ann, please sign by Friday.\", \"expiry_date\": \"2026-12-31\"}' # 7. Poll until status is 6 (Completed), then get the signed PDF curl \"$API/ap1/documents-status-list/?document_ids=$DOC\" -H \"Authorization: Bearer $TOKEN\" curl \"$API/ap1/document/download-link/?id=$DOC\" -H \"Authorization: Bearer $TOKEN\" ``` ## Postman Import the whole API into Postman with **Import → Link** and `<this server>/swaggerap1/postman.json`. Set `client_id` and `client_secret` in the collection's **Variables**, run **Get an access token** (it saves the token for every other request), and set `document_id` when a request needs one. ## Webhooks Instead of polling, add an endpoint with `POST /ap1/webhooks/` (or in **Account & Settings → API**) and Signedly posts your organisation's document events to it: `document.sent`, `document.viewed`, `document.signed`, `document.approved`, `document.completed`, `document.rejected` and `document.expired` (all of them, or the `events` you choose). Each is a JSON `POST`: ```json {\"id\": \"5b7f…\", \"type\": \"document.signed\", \"created_at\": \"2026-10-27T09:12:00+00:00\", \"test\": false, \"data\": {\"document\": {\"id\": \"6f0c…\", \"name\": \"nda.pdf\", \"status\": 5}, \"recipient\": {\"id\": \"0b6e…\", \"email\": \"ann@example.com\", \"name\": \"Ann Lee\", \"role\": \"signer\"}}} ``` `recipient` is null for events no recipient caused (sent, completed, expired); `test` is true for documents made with your test keys. Fetch anything else, such as download links, with your access token: events never carry links. **Check every event is from Signedly.** The `Signedly-Signature` header is `t=<unix seconds>,v1=<signature>`, where the signature is the hex HMAC-SHA256 of `<t>.<raw body>` with the endpoint's secret. Refuse an event whose signature doesn't match or whose `t` is more than 5 minutes off: ```python import hashlib, hmac, time def is_from_signedly(secret, header, raw_body): parts = dict(item.split(\"=\", 1) for item in header.split(\",\")) signed = f\"{parts['t']}.{raw_body.decode()}\".encode() expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() fresh = abs(time.time() - int(parts[\"t\"])) <= 300 return fresh and hmac.compare_digest(parts[\"v1\"], expected) ``` **Answer 2xx within 10 seconds**, then do the work. Anything else is retried after 1 and 5 minutes, then 30 minutes and 2, 5 and 10 hours (7 tries in about 18 hours). `Signedly-Event-Id` is the same on every try of an event: use it to ignore repeats. After 20 events in a row fail for good, Signedly switches the endpoint off and emails your organisation's admin; switch it on again with `PUT /ap1/webhooks/{id}/` and `\"is_active\": true`. Each endpoint's deliveries and their last answers are at `GET /ap1/webhooks/{id}/deliveries/`, and any can be sent again. ## Sandbox Build and test against the API without paying for documents: in **Account & Settings → API**, generate your **test keys**, and get a token with them at `/ap1/token/` as usual. Everything works the same, except that what you upload or make from a template with a test token is a **test document** (`is_test` on its upload): - every page of its PDF and of its audit trail says **TEST – NOT LEGALLY BINDING**; - its emails are real, with `[TEST]` at the start of the subject; - it isn't billed or counted toward your plan; - your organisation can make 50 a calendar month (templates don't count); the next upload answers 429. ## Fields Positions and sizes are in **PDF points** (1/72 inch), measured from the **top-left corner** of the page as it is displayed (rotation applied). `page_number` counts pages from 0. A field must sit wholly on its page (1 point of tolerance); otherwise it is refused with 400. A field's page and signatory can't be changed later: delete the field and add it again. ## Status values **Document `status`:** 1 Draft · 2 Signees Added · 3 Prepared · 4 Sent · 5 In Progress · 6 Completed · 7 Rejected · 8 Archived · 9 Expired. Signatories, fields and sends are only accepted while a document is 1–3 (not yet sent). **Signatory `status`:** 1 Waiting · 2 Invited · 3 Signed · 4 Viewed · 5 Rejected. ## Lists `GET /ap1/documents/`, `/ap1/documents-status-list/`, `/ap1/templates/`, `/ap1/logs/` and `/ap1/logs/hits/` are paged: pass `limit` (default 10) and `offset`, and read `count`, `next`, `previous` and `results` from the response. ## Errors | Status | Meaning | |---|---| | 400 | The request was refused, also when your plan doesn't include it (see Plans); the body says why (`{\"error\": \"...\"}`, or the name of each bad field with its messages). Nothing was changed. | | 403 | The access token is missing, unknown or expired; the client ID and secret or the refresh token were refused; the organisation's plan has no API access (Free, see Plans); or an `expiry_date` isn't in the future (`\"Invalid Date Entered\"`). | | 402 | No API contract, or an unpaid API invoice: live documents are refused (test keys keep working). | | 404 | The document, template, signatory or field doesn't exist or isn't yours. | | 429 | Too many token requests (wait a minute), or this month's 50 test documents are used. | | 5xx | Our side failed; retry later. | A send that times out may still have worked: check the document's status before sending again (a second send of a sent document answers 400). Download, view and audit links are valid for about 7 days; ask for a new one whenever you need it.
|
|
7
|
+
|
|
8
|
+
The version of the OpenAPI document: v1
|
|
9
|
+
Contact: info@signedly.com
|
|
10
|
+
Generated by OpenAPI Generator (https://openapi-generator.tech)
|
|
11
|
+
|
|
12
|
+
Do not edit the class manually.
|
|
13
|
+
""" # noqa: E501
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
import pprint
|
|
18
|
+
import re # noqa: F401
|
|
19
|
+
import json
|
|
20
|
+
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictInt, StrictStr
|
|
23
|
+
from typing import Any, ClassVar, Dict, List, Optional
|
|
24
|
+
from typing_extensions import Annotated
|
|
25
|
+
from signedly.models.page import Page
|
|
26
|
+
from signedly.models.signatory import Signatory
|
|
27
|
+
from signedly.models.upload import Upload
|
|
28
|
+
from typing import Optional, Set
|
|
29
|
+
from typing_extensions import Self
|
|
30
|
+
|
|
31
|
+
class Document(BaseModel):
|
|
32
|
+
"""
|
|
33
|
+
Document
|
|
34
|
+
""" # noqa: E501
|
|
35
|
+
created_at: Optional[datetime] = Field(default=None, description="When it was made.")
|
|
36
|
+
document_name: Optional[StrictStr] = Field(default=None, description="The document's name.")
|
|
37
|
+
expired_at: Optional[datetime] = Field(default=None, description="The last day it can be signed, or null.")
|
|
38
|
+
id: Optional[StrictStr] = Field(default=None, description="The document's ID.")
|
|
39
|
+
is_bulk_signing: Optional[StrictBool] = Field(default=None, description="True for a copy made by a bulk send.")
|
|
40
|
+
is_template: Optional[StrictBool] = Field(default=None, description="Always false here.")
|
|
41
|
+
message: Optional[StrictStr] = Field(default=None, description="The signing email's message, once sent.")
|
|
42
|
+
pages: Optional[List[Page]] = Field(default=None, description="Its pages, each with its fields.")
|
|
43
|
+
sent_at: Optional[datetime] = Field(default=None, description="When it was sent, or null.")
|
|
44
|
+
signatories: Optional[List[Signatory]] = Field(default=None, description="Who signs it.")
|
|
45
|
+
status: Optional[StrictInt] = Field(default=None, description="1 Draft · 2 Signees Added · 3 Prepared · 4 Sent · 5 In Progress · 6 Completed · 7 Rejected · 8 Archived · 9 Expired")
|
|
46
|
+
subject: Optional[Annotated[str, Field(min_length=1, strict=True, max_length=200)]] = Field(default=None, description="The signing email's subject, once sent.")
|
|
47
|
+
updated_at: Optional[datetime] = Field(default=None, description="When it last changed.")
|
|
48
|
+
upload: Optional[Upload] = None
|
|
49
|
+
__properties: ClassVar[List[str]] = ["created_at", "document_name", "expired_at", "id", "is_bulk_signing", "is_template", "message", "pages", "sent_at", "signatories", "status", "subject", "updated_at", "upload"]
|
|
50
|
+
|
|
51
|
+
model_config = ConfigDict(
|
|
52
|
+
populate_by_name=True,
|
|
53
|
+
validate_assignment=True,
|
|
54
|
+
protected_namespaces=(),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def to_str(self) -> str:
|
|
59
|
+
"""Returns the string representation of the model using alias"""
|
|
60
|
+
return pprint.pformat(self.model_dump(by_alias=True))
|
|
61
|
+
|
|
62
|
+
def to_json(self) -> str:
|
|
63
|
+
"""Returns the JSON representation of the model using alias"""
|
|
64
|
+
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
|
|
65
|
+
return json.dumps(self.to_dict())
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def from_json(cls, json_str: str) -> Optional[Self]:
|
|
69
|
+
"""Create an instance of Document from a JSON string"""
|
|
70
|
+
return cls.from_dict(json.loads(json_str))
|
|
71
|
+
|
|
72
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
73
|
+
"""Return the dictionary representation of the model using alias.
|
|
74
|
+
|
|
75
|
+
This has the following differences from calling pydantic's
|
|
76
|
+
`self.model_dump(by_alias=True)`:
|
|
77
|
+
|
|
78
|
+
* `None` is only added to the output dict for nullable fields that
|
|
79
|
+
were set at model initialization. Other fields with value `None`
|
|
80
|
+
are ignored.
|
|
81
|
+
* OpenAPI `readOnly` fields are excluded.
|
|
82
|
+
* OpenAPI `readOnly` fields are excluded.
|
|
83
|
+
* OpenAPI `readOnly` fields are excluded.
|
|
84
|
+
* OpenAPI `readOnly` fields are excluded.
|
|
85
|
+
"""
|
|
86
|
+
excluded_fields: Set[str] = set([
|
|
87
|
+
"created_at",
|
|
88
|
+
"document_name",
|
|
89
|
+
"id",
|
|
90
|
+
"updated_at",
|
|
91
|
+
])
|
|
92
|
+
|
|
93
|
+
_dict = self.model_dump(
|
|
94
|
+
by_alias=True,
|
|
95
|
+
exclude=excluded_fields,
|
|
96
|
+
exclude_none=True,
|
|
97
|
+
)
|
|
98
|
+
# override the default output from pydantic by calling `to_dict()` of each item in pages (list)
|
|
99
|
+
_items = []
|
|
100
|
+
if self.pages:
|
|
101
|
+
for _item_pages in self.pages:
|
|
102
|
+
if _item_pages:
|
|
103
|
+
_items.append(_item_pages.to_dict())
|
|
104
|
+
_dict['pages'] = _items
|
|
105
|
+
# override the default output from pydantic by calling `to_dict()` of each item in signatories (list)
|
|
106
|
+
_items = []
|
|
107
|
+
if self.signatories:
|
|
108
|
+
for _item_signatories in self.signatories:
|
|
109
|
+
if _item_signatories:
|
|
110
|
+
_items.append(_item_signatories.to_dict())
|
|
111
|
+
_dict['signatories'] = _items
|
|
112
|
+
# override the default output from pydantic by calling `to_dict()` of upload
|
|
113
|
+
if self.upload:
|
|
114
|
+
_dict['upload'] = self.upload.to_dict()
|
|
115
|
+
# set to None if expired_at (nullable) is None
|
|
116
|
+
# and model_fields_set contains the field
|
|
117
|
+
if self.expired_at is None and "expired_at" in self.model_fields_set:
|
|
118
|
+
_dict['expired_at'] = None
|
|
119
|
+
|
|
120
|
+
# set to None if message (nullable) is None
|
|
121
|
+
# and model_fields_set contains the field
|
|
122
|
+
if self.message is None and "message" in self.model_fields_set:
|
|
123
|
+
_dict['message'] = None
|
|
124
|
+
|
|
125
|
+
# set to None if sent_at (nullable) is None
|
|
126
|
+
# and model_fields_set contains the field
|
|
127
|
+
if self.sent_at is None and "sent_at" in self.model_fields_set:
|
|
128
|
+
_dict['sent_at'] = None
|
|
129
|
+
|
|
130
|
+
return _dict
|
|
131
|
+
|
|
132
|
+
@classmethod
|
|
133
|
+
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
|
|
134
|
+
"""Create an instance of Document from a dict"""
|
|
135
|
+
if obj is None:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
if not isinstance(obj, dict):
|
|
139
|
+
return cls.model_validate(obj)
|
|
140
|
+
|
|
141
|
+
_obj = cls.model_validate({
|
|
142
|
+
"created_at": obj.get("created_at"),
|
|
143
|
+
"document_name": obj.get("document_name"),
|
|
144
|
+
"expired_at": obj.get("expired_at"),
|
|
145
|
+
"id": obj.get("id"),
|
|
146
|
+
"is_bulk_signing": obj.get("is_bulk_signing"),
|
|
147
|
+
"is_template": obj.get("is_template"),
|
|
148
|
+
"message": obj.get("message"),
|
|
149
|
+
"pages": [Page.from_dict(_item) for _item in obj["pages"]] if obj.get("pages") is not None else None,
|
|
150
|
+
"sent_at": obj.get("sent_at"),
|
|
151
|
+
"signatories": [Signatory.from_dict(_item) for _item in obj["signatories"]] if obj.get("signatories") is not None else None,
|
|
152
|
+
"status": obj.get("status"),
|
|
153
|
+
"subject": obj.get("subject"),
|
|
154
|
+
"updated_at": obj.get("updated_at"),
|
|
155
|
+
"upload": Upload.from_dict(obj["upload"]) if obj.get("upload") is not None else None
|
|
156
|
+
})
|
|
157
|
+
return _obj
|
|
158
|
+
|
|
159
|
+
|