API
Signed Documents

9. Signed Documents

Prerequisites:

1. Create Project entity

When creating a new project, a Strapi lifecycle hook (beforeCreate) is executed. This hook generates a new folder in Signius and assigns its ID to the project's externalDocumentsFolderId field. This ID is subsequently used when creating new sign contracts.

2. Create DOCX Document(s) with Defined Fields

Currently Supported Fields:

User Data:

  • firstName
  • lastName
  • birthDate
  • phone
  • street
  • postalCode
  • addressExtra
  • city
  • country
  • email

Additional fields (values passed from frontend application with createSignContracts mutation):

  • amount

Inside document field needs to be added in curly braces eg.: {firstName}

Example document with placeholders:

DocumentWithPlaceholders

3. Create a Contract for Each Document

  • In Strapi admin, create a new Contract entity.
  • Attach the DOCX document to the document field.
  • Add a relation to the selected Project entity.
CreateContract

4. Add autoSigners to rentinvesto Single Type in strapi admin

It needs to be data of a user already created in signius and that has apiKey generated.

[
  {
    "name": "",
    "surname": "",
    "signeeEmail": "",
    "signeePhone": "123456789" // optional, not needed for automatic signing
  }
]

5. Add signius variables to secrects in rentinvesto Single Type in strapi admin

{
  "SIGNIUS_DOCUMENT_CERT": "",
  "SIGNIUS_IDENTITY_CERT": "",
  "SIGNIUS_IDENTITY_PASS": "",
  "SIGNIUS_IDENTITY_BASE_URL": "",
  "SIGNIUS_DOCUMENTS_BASE_URL": "",
  "SIGNIUS_API_KEY": "",
  "SIGNIUS_AUTO_SIGN_API_KEYS": [] // apiKeys related to autoSigners data, needs to be in the same order in the list
}

Signius Integration:

Common arguments:

platform: 'RENTINVESTO' | 'CROWDEX'

1. checkSignContracts

This query should be used first, before investment process - it returns information about current sign contracts, if there are some pending or not signed - in that case we should process previously started investment process before going with the new one.

It also can return error that some user data is missing, which means he first needs to go and update data before being able to invest. Required user data:

  • firstName
  • lastName
  • birthDate
  • phone
  • street
  • postalCode
  • addressExtra
  • city
  • country
  • email
query CheckSignContractsQuery($projectId: String!, $locale: String!) {
  checkSignContracts(projectId: $projectId, locale: $locale) {
    status
    signContracts {
      data {
        id
        attributes {
          status
          externalId
          createdAt
          signedAt
          contract {
            data {
              attributes {
                externalId
                name
              }
            }
          }
          document {
            data {
              attributes {
                url
              }
            }
          }
        }
      }
    }
  }
}

possible status: SIGNED, NOT_SIGNED, EMPTY_CONTRACTS, PENDING

Authorization: Bearer ACCESS_TOKEN

Responses:

If all sign contracts for given user are signed:

{
  "data": {
    "checkSignContracts": {
      "status": "SIGNED",
      "signContracts": null
    }
  }
}

If there are no sign contracts for given user:

{
  "data": {
    "checkSignContracts": {
      "status": "EMPTY_CONTRACTS",
      "signContracts": null
    }
  }
}

If there are contracts that are not signed:

{
  "data": {
    "checkSignContracts": {
      "status": "NOT_SIGNED",
      "signContracts": {
        "data": [
          {
            "id": "77",
            "attributes": {
              "status": "NOT_SIGNED",
              "externalId": "bbe6292a-5bbb-4dad-bd22-8f79a4bda550",
              "createdAt": "2024-07-22T20:24:05.813Z",
              "signedAt": null,
              "contract": {
                "data": {
                  "attributes": {
                    "externalId": null,
                    "name": "Document number 2"
                  }
                }
              },
              "document": {
                "data": {
                  "attributes": {
                    "url": "/uploads/Crowdex_Test_Document_6c059d0732_9_5af6.pdf"
                  }
                }
              }
            }
          }
        ]
      }
    }
  }
}

If there are contracts that are pending:

{
  "data": {
    "checkSignContracts": {
      "status": "PENDING",
      "signContracts": {
        "data": [
          {
            "id": "77",
            "attributes": {
              "status": "PENDING",
              "externalId": "bbe6292a-5bbb-4dad-bd22-8f79a4bda550",
              "createdAt": "2024-07-22T20:24:05.813Z",
              "signedAt": null,
              "contract": {
                "data": {
                  "attributes": {
                    "externalId": null,
                    "name": "Document number 2"
                  }
                }
              },
              "document": {
                "data": {
                  "attributes": {
                    "url": "/uploads/Crowdex_Test_Document_6c059d0732_9_5af6.pdf"
                  }
                }
              }
            }
          }
        ]
      }
    }
  }
}

Missing user data:

{
  "errors": [
    {
      "message": "MISSING_USER_DATA",
      "extensions": {
        "error": {
          "name": "ApplicationError",
          "message": "MISSING_USER_DATA",
          "details": {}
        },
        "code": "STRAPI_APPLICATION_ERROR"
      }
    }
  ],
  "data": null
}

2. (Option 1 - not used anymore) createSignContracts mutation is responsible for:

- check if all required userData is filled

- finding all Contract records required for given Project,

- creating new user in signius and updating user entity with signius user externalId

- creating relation between signContracts and transaction with given transactionId

- generating filled pdfs with user data (from docx template attached to related Contract entity) and uploading it to the server

- creating SignContract, ass relation to uploaded document,

- creating document instance in signius, updating it with signers data (user + admin, in that order)

- update SignContract with signus document id as externalId

mutation CreateSignContracts($projectId: String!, $locale: String!, transactionId: string, $additionalDocumentData: JSONObject!, $platform: String!) {
  createSignContracts(projectId: $projectId, locale: $locale, transactionId: $transactionId, additionalDocumentData: $additionalDocumentData, platform: $platform) {
    data {
      id
      attributes {
        status
        externalId
        createdAt
        signedAt
        contract {
          data {
            attributes {
              externalId
              name
            }
          }
        }
        document {
          data {
            attributes {
              url
            }
          }
        }
      }
    }
  }
}

Authorization: Bearer ACCESS_TOKEN

additionalDocumentData argument should be treated as Record<string,string> typescript type. It is used to pass additional data that will be used to prefill specified fields in docx template. Check in Prerequisites for supported fields.

Responses:

Proper one:

{
  "data": {
    "createSignContracts": {
      "data": [
        {
          "id": "76",
          "attributes": {
            "status": "NOT_SIGNED",
            "externalId": "97cacdfc-373e-4dcc-8004-cdb319af63d9",
            "createdAt": "2024-07-22T20:24:05.109Z",
            "signedAt": null,
            "contract": {
              "data": {
                "attributes": {
                  "externalId": null,
                  "name": "Needed Document"
                }
              }
            },
            "document": {
              "data": {
                "attributes": {
                  "url": "/uploads/Crowdex_Test_Document_6c059d0732_9_64ac.pdf"
                }
              }
            }
          }
        }
      ]
    }
  }
}

Missing user data:

{
  "errors": [
    {
      "message": "MISSING_USER_DATA",
      "extensions": {
        "error": {
          "name": "ApplicationError",
          "message": "MISSING_USER_DATA",
          "details": {}
        },
        "code": "STRAPI_APPLICATION_ERROR"
      }
    }
  ],
  "data": null
}

2. (Option 2 - used in crowdex/rentinvesto webapp ) - createPurchase mutation

Same as Option 1, these mutation apart for creating transaction is responsible for:

- check if all required userData is filled

- finding all Contract records required for given Project, then:

If there are some contracts for given project:

- creating new user in signius and updating user entity with signius user externalId

- creating relation between signContracts and transaction with given transactionId

- generating filled pdfs with user data (from docx template attached to related Contract entity) and uploading it to the server

- creating SignContract, ass relation to uploaded document,

- creating document instance in signius, updating it with signers data (user + admin, in that order)

- update SignContract with signus document id as externalId

- setting transaction in REQUIRES_CONTRACTS_SIGNING state

If there are no contracts for given project:

- setting transaction in PENDING state

mutation createTransaction(
    $projectId: String!
    $amount: Float!
    $locale: String!
    $additionalDocumentData: JSONObject!
  ) {
    createPurchase(
      projectId: $projectId
      platform: "RENTINVESTO" // -> RENTINVESTO  or CROWDEX
      amount: $amount
      locale: $locale
      additionalDocumentData: $additionalDocumentData
    ) {
      correlationId
      signContracts {
        data {
          id
          attributes {
            status
            externalId
            createdAt
            signedAt
            contract {
              data {
                attributes {
                  externalId
                  name
                  project {
                    data {
                      attributes {
                        title
                      }
                    }
                  }
                }
              }
            }
            document {
              data {
                attributes {
                  url
                }
              }
            }
          }
        }
      }
    }
  }

Authorization: Bearer ACCESS_TOKEN

additionalDocumentData argument should be treated as Record<string,string> typescript type. It is used to pass additional data that will be used to prefill specified fields in docx template. Check in Prerequisites for supported fields.

3. createSigningSession mutation is used to create signing session in signus, it will send SMS to user phone number with code to sign the document.

mutation CreateSigningSession($signContractIds: [String!]!, $platform: String!) {
  createSigningSession(signContractIds: $signContractIds, platform: $platform) {
    batchIds
  }
}

Authorization: Bearer ACCESS_TOKEN

signContractIds - list of signContracts ids, which will be signed during this session

Responses:

{
  "data": {
    "createSigningSession": {
      "batchIds": ["b42fecb0-0cb5-43f2-aaea-0e199a2e33d5"]
    }
  }
}

batchIds - these are used in next mutation to process contract signing

Calling for sign contracts that were already signed:

{
  "errors": [
    {
      "message": "CONTRACTS_ALREADY_SIGNED",
      "extensions": {
        "error": {
          "name": "ConflictError",
          "message": "CONTRACTS_ALREADY_SIGNED"
        },
        "code": "STRAPI_CONFLICT_ERROR"
      }
    }
  ],
  "data": {
    "createSigningSession": null
  }
}

4. processSigning mutation is used to sign documents with code from SMS

mutation ProcessSigning($batchIds: [String!]!, $signContractIds: [String!]!, $code: String!, $platform: String!) {
  processSigning(batchIds: $batchIds, signContractIds: $signContractIds, code: $code, platform: $platform) {
    status
  }
}

Authorization: Bearer ACCESS_TOKEN

signContractIds - list of signContracts ids, the same that were used in createSigningSession mutation - it is needed so we know which sign contracts status should be changed to PENDING

batchIds - response from the createSigningSession mutation

code - sms code

Responses:

If everything went ok:

{
  "data": {
    "processSigning": {
      "status": "OK"
    }
  }
}

When wrong sms code has been provided:

{
  "errors": [
    {
      "message": "INCORRECT_CODE",
      "extensions": {
        "error": {
          "name": "ApplicationError",
          "message": "INCORRECT_CODE",
          "details": {}
        },
        "code": "STRAPI_APPLICATION_ERROR"
      }
    }
  ],
  "data": {
    "processSigning": null
  }
}

5. signedContractsByIds query - used to retrieve sign contracts by ids, the response is diferent for each status. This query should be used for polling to check status of contracts after user signed contracts with sms code.

query SignedContractsByIds($signContractIds: [String!]!) {
  signedContractsByIds(signContractIds: $signContractIds) {
    status
    signContracts {
      data {
        id
        attributes {
          status
          signedAt
          contract {
            data {
              attributes {
                externalId
                name
              }
            }
          }
          document {
            data {
              attributes {
                url
              }
            }
          }
        }
      }
    }
  }
}

Authorization: Bearer ACCESS_TOKEN

signContractIds - list of signContracts ids, which we want to get

Responses:

If all contracts are signed:

{
  "data": {
    "signedContractsByIds": {
      "status": "SIGNED",
      "signContracts": {
        "data": [
          {
            "id": "75",
            "attributes": {
              "status": "SIGNED",
              "signedAt": "2024-07-22T19:36:16.132Z",
              "contract": {
                "data": {
                  "attributes": {
                    "externalId": null,
                    "name": "Needed Document"
                  }
                }
              },
              "document": {
                "data": {
                  "attributes": {
                    "url": "/uploads/Crowdex_Test_Document_6c059d0732_9_8515373f04_signed.pdf"
                  }
                }
              }
            }
          }
        ]
      }
    }
  }
}

If some contracts are in PENDING state:

{
  "data": {
    "checkSignContracts": {
      "status": "PENDING",
      "signContracts": null
    }
  }
}

If there are no sign contracts with given ids:

{
  "data": {
    "checkSignContracts": {
      "status": "EMPTY_CONTRACTS",
      "signContracts": null
    }
  }
}

If there are sign contracts that are with NOT_SIGNED status (but there are not with PENDING state):

{
  "data": {
    "signedContractsByIds": {
      "status": "NOT_SIGNED",
      "signContracts": {
        "data": [
          {
            "id": "77",
            "attributes": {
              "status": "NOT_SIGNED",
              "signedAt": null,
              "contract": {
                "data": {
                  "attributes": {
                    "externalId": null,
                    "name": "Document number 2"
                  }
                }
              },
              "document": {
                "data": {
                  "attributes": {
                    "url": "/uploads/Crowdex_Test_Document_6c059d0732_9_5af6.pdf"
                  }
                }
              }
            }
          }
        ]
      }
    }
  }
}

6. allSignContracts query - returns all signContracts for given user with chosen project data

query AllSignContracts {
  allSignContracts {
    data {
      id
      attributes {
        status
        externalId
        createdAt
        signedAt
        contract {
          data {
            attributes {
              externalId
              name
              project {
                data {
                  id
                  attributes {
                    title
                  }
                }
              }
              crowdexProject {
                data {
                  id
                  attributes {
                    title
                  }
                }
              }
            }
          }
        }
        document {
          data {
            attributes {
              url
            }
          }
        }
      }
    }
  }
}

Authorization: Bearer ACCESS_TOKEN

Response:

{
  "data": {
    "allSignContracts": {
      "data": [
        {
          "id": "70",
          "attributes": {
            "status": "SIGNED",
            "externalId": "cf33bf51-aa66-425d-838c-a6345297c677",
            "createdAt": "2024-07-22T18:43:42.559Z",
            "signedAt": null,
            "contract": {
              "data": {
                "attributes": {
                  "externalId": null,
                  "name": "Needed Document",
                  "project": {
                    "data": {
                      "id": "2",
                      "attributes": {
                        "title": "ProjectWithSignus"
                      }
                    }
                  },
                  "crowdexProject": {
                    "data": {
                      "id": "5",
                      "attributes": {
                        "title": "Great project with Signus"
                      }
                    }
                  }
                }
              }
            },
            "document": {
              "data": {
                "attributes": {
                  "url": "/uploads/Crowdex_Test_Document_6c059d0732_9_625e921102_signed_signed_signed_signed.pdf"
                }
              }
            }
          }
        }
      ]
    }
  }
}

removeSignContracts mutation is used to delete signContracts records, remove documents from the server and remove documents on signius side

mutation RemoveSignContracts($platform: String!, $signContractIds: [String!]!) {
  removeSignContracts(platform: $platform, signContractIds: $signContractIds) {
    status
  }
}

Authorization: Bearer ACCESS_TOKEN

signContractIds - list of signContracts ids, which we want to delete

Response:

{
  "data": {
    "removeSignContracts": {
      "status": "OK"
    }
  }
}

Signius webhook:

Url: {backendBaseUrl}/api/signius/webhook

Authroization: No Auth

Response body:

  1. After user signed the document:
{
  "documentId": "55609398-22b5-4a1f-89a8-66049d93ac84", --> SignContract externalId
  "presentSignee": "69dcbd9d-47e6-4cc1-891a-29f5f8e80a04", --> User externalId
  "nextSignee": "", --> not important in our flow, because we are using automatic signing for the rest of the signers
  "documentStatus": "SIGNING_IN_PROGRESS"
}

In webhook method following operations are happening:

  • automatic signing in signius - for each apiKey from SIGNIUS_AUTO_SIGN_API_KEYS secrets variable
  • current document for SignContract entity is replaced with the new one (signed, downloaded from signius)
  • SignContract entity status is changed to SIGNED
  • Related Transaction status is changed from 'REQUIRES_CONTRACTS_SIGNING' to 'PENDING'