diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..348a99484 Binary files /dev/null and b/.DS_Store differ diff --git a/api-reference/accounts/create.mdx b/api-reference/accounts/create.mdx new file mode 100644 index 000000000..efb0162c4 --- /dev/null +++ b/api-reference/accounts/create.mdx @@ -0,0 +1,4 @@ +--- +title: 'Create an account' +openapi: 'POST /accounts' +--- diff --git a/api-reference/accounts/delete.mdx b/api-reference/accounts/delete.mdx new file mode 100644 index 000000000..0703dc6f3 --- /dev/null +++ b/api-reference/accounts/delete.mdx @@ -0,0 +1,4 @@ +--- +title: 'Delete an account' +openapi: 'DELETE /accounts/{account_id}' +--- diff --git a/api-reference/accounts/get.mdx b/api-reference/accounts/get.mdx new file mode 100644 index 000000000..1074b018f --- /dev/null +++ b/api-reference/accounts/get.mdx @@ -0,0 +1,4 @@ +--- +title: 'Get an account' +openapi: 'GET /accounts/{account_id}' +--- \ No newline at end of file diff --git a/api-reference/accounts/list-recurring.mdx b/api-reference/accounts/list-recurring.mdx new file mode 100644 index 000000000..98ad80e75 --- /dev/null +++ b/api-reference/accounts/list-recurring.mdx @@ -0,0 +1,4 @@ +--- +title: 'List all recurring accounts' +openapi: 'GET /accounts/recurring' +--- \ No newline at end of file diff --git a/api-reference/accounts/list.mdx b/api-reference/accounts/list.mdx new file mode 100644 index 000000000..35f59669a --- /dev/null +++ b/api-reference/accounts/list.mdx @@ -0,0 +1,4 @@ +--- +title: 'List all accounts' +openapi: 'GET /accounts' +--- \ No newline at end of file diff --git a/api-reference/api-keys/create.mdx b/api-reference/api-keys/create.mdx new file mode 100644 index 000000000..85eb3502c --- /dev/null +++ b/api-reference/api-keys/create.mdx @@ -0,0 +1,4 @@ +--- +title: 'Create an API Key' +openapi: 'POST /api-keys' +--- diff --git a/api-reference/api-keys/delete.mdx b/api-reference/api-keys/delete.mdx new file mode 100644 index 000000000..007bd17c6 --- /dev/null +++ b/api-reference/api-keys/delete.mdx @@ -0,0 +1,4 @@ +--- +title: 'Delete an API Key' +openapi: 'DELETE /api-keys/{api_key_id}' +--- diff --git a/api-reference/api-keys/list.mdx b/api-reference/api-keys/list.mdx new file mode 100644 index 000000000..51913a143 --- /dev/null +++ b/api-reference/api-keys/list.mdx @@ -0,0 +1,4 @@ +--- +title: 'List API Keys' +openapi: 'GET /api-keys' +--- diff --git a/api-reference/configuration/get.mdx b/api-reference/configuration/get.mdx new file mode 100644 index 000000000..151509cb8 --- /dev/null +++ b/api-reference/configuration/get.mdx @@ -0,0 +1,6 @@ +--- +title: 'Get client configuration' +openapi: 'GET /configuration' +--- + + diff --git a/api-reference/configuration/update.mdx b/api-reference/configuration/update.mdx new file mode 100644 index 000000000..55a3fb713 --- /dev/null +++ b/api-reference/configuration/update.mdx @@ -0,0 +1,6 @@ +--- +title: 'Update client configuration' +openapi: 'PUT /configuration' +--- + + diff --git a/api-reference/documents/delete.mdx b/api-reference/documents/delete.mdx new file mode 100644 index 000000000..0f73e6eaa --- /dev/null +++ b/api-reference/documents/delete.mdx @@ -0,0 +1,4 @@ +--- +title: 'Delete a document' +openapi: 'DELETE /documents/{document_id}' +--- \ No newline at end of file diff --git a/api-reference/documents/get.mdx b/api-reference/documents/get.mdx new file mode 100644 index 000000000..a7857e1cb --- /dev/null +++ b/api-reference/documents/get.mdx @@ -0,0 +1,4 @@ +--- +title: 'Retrieve a specific document' +openapi: 'GET /documents/{document_id}' +--- \ No newline at end of file diff --git a/api-reference/documents/list.mdx b/api-reference/documents/list.mdx new file mode 100644 index 000000000..bfc6a62cc --- /dev/null +++ b/api-reference/documents/list.mdx @@ -0,0 +1,4 @@ +--- +title: 'Lists all documents' +openapi: 'GET /documents' +--- \ No newline at end of file diff --git a/api-reference/endpoint/create.mdx b/api-reference/endpoint/create.mdx deleted file mode 100644 index 5689f1b65..000000000 --- a/api-reference/endpoint/create.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Create Plant' -openapi: 'POST /plants' ---- diff --git a/api-reference/endpoint/delete.mdx b/api-reference/endpoint/delete.mdx deleted file mode 100644 index 657dfc871..000000000 --- a/api-reference/endpoint/delete.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Delete Plant' -openapi: 'DELETE /plants/{id}' ---- diff --git a/api-reference/endpoint/get.mdx b/api-reference/endpoint/get.mdx deleted file mode 100644 index 56aa09ec1..000000000 --- a/api-reference/endpoint/get.mdx +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Get Plants' -openapi: 'GET /plants' ---- diff --git a/api-reference/entries/connections-create.mdx b/api-reference/entries/connections-create.mdx new file mode 100644 index 000000000..e65ad2429 --- /dev/null +++ b/api-reference/entries/connections-create.mdx @@ -0,0 +1,4 @@ +--- +title: 'Create a connection' +openapi: 'POST /entries/connections' +--- diff --git a/api-reference/entries/delete.mdx b/api-reference/entries/delete.mdx new file mode 100644 index 000000000..3168b472c --- /dev/null +++ b/api-reference/entries/delete.mdx @@ -0,0 +1,4 @@ +--- +title: 'Delete an entry' +openapi: 'DELETE /entries/{entry_id}' +--- \ No newline at end of file diff --git a/api-reference/entries/list.mdx b/api-reference/entries/list.mdx new file mode 100644 index 000000000..74031fc8a --- /dev/null +++ b/api-reference/entries/list.mdx @@ -0,0 +1,4 @@ +--- +title: 'List all entries' +openapi: 'GET /entries' +--- diff --git a/api-reference/entries/payslips-create.mdx b/api-reference/entries/payslips-create.mdx new file mode 100644 index 000000000..3f94ef16e --- /dev/null +++ b/api-reference/entries/payslips-create.mdx @@ -0,0 +1,199 @@ +--- +title: 'Upload payslips' +openapi: 'POST /entries/payslips' +--- + + + + + +```bash cURL +curl --request POST \ + --url https://api.sandbox.goteal.co/entries/payslips \ + --header 'Authorization: Bearer ' \ + --header 'Content-Type: multipart/form-data' \ + --form 'payslip123456=@"/local/file/file1-payslip.pdf"' + --form 'payslip2=@"/local/file/John-Smith-payslip18.pdf.pdf"' + --form 'payslip98765=@"/local/file/Payslip3.pdf"' +``` + +```python Python +import requests + +url = "https://api.sandbox.goteal.co/entries/payslips" + +payload = "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"payslip\"\r\n\r\n@\"/local/file/system/path/to/payslip/Payslip_44404148.pdf\"\n\r\n-----011000010111000001101001--\r\n\r\n" +headers = { + "Authorization": "Bearer ", + "Content-Type": "multipart/form-data" +} + +response = requests.request("POST", url, data=payload, headers=headers) + +print(response.text) + +``` + + +```javascript JavaScript +const form = new FormData(); +form.append("payslip", "@\"/local/file/system/path/to/payslip/Payslip_44404148.pdf\"\n"); + +const options = { + method: 'POST', + headers: {Authorization: 'Bearer ', 'Content-Type': 'multipart/form-data'} +}; + +options.body = form; + +fetch('https://api.sandbox.goteal.co/entries/payslips', options) + .then(response => response.json()) + .then(response => console.log(response)) + .catch(err => console.error(err)); +``` + + +```php PHP + "https://api.sandbox.goteal.co/entries/payslips", + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => "", + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => "POST", + CURLOPT_POSTFIELDS => "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"payslip\"\r\n\r\n@\"/local/file/system/path/to/payslip/Payslip_44404148.pdf\"\n\r\n-----011000010111000001101001--\r\n\r\n", + CURLOPT_HTTPHEADER => [ + "Authorization: Bearer ", + "Content-Type: multipart/form-data" + ], +]); + +$response = curl_exec($curl); +$err = curl_error($curl); + +curl_close($curl); + +if ($err) { + echo "cURL Error #:" . $err; +} else { + echo $response; +} +``` + + + +```go Go +package main + +import ( + "fmt" + "strings" + "net/http" + "io/ioutil" +) + +func main() { + + url := "https://api.sandbox.goteal.co/entries/payslips" + + payload := strings.NewReader("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"payslip\"\r\n\r\n@\"/local/file/system/path/to/payslip/Payslip_44404148.pdf\"\n\r\n-----011000010111000001101001--\r\n\r\n") + + req, _ := http.NewRequest("POST", url, payload) + + req.Header.Add("Authorization", "Bearer ") + req.Header.Add("Content-Type", "multipart/form-data") + + res, _ := http.DefaultClient.Do(req) + + defer res.Body.Close() + body, _ := ioutil.ReadAll(res.Body) + + fmt.Println(res) + fmt.Println(string(body)) + +} +``` + + + +```java Java +HttpResponse response = Unirest.post("https://api.sandbox.goteal.co/entries/payslips") + .header("Authorization", "Bearer ") + .header("Content-Type", "multipart/form-data") + .body("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"payslip\"\r\n\r\n@\"/local/file/system/path/to/payslip/Payslip_44404148.pdf\"\n\r\n-----011000010111000001101001--\r\n\r\n") + .asString(); +``` + + + + + +```json Response +{ + "account_id": "95a0e70b-fe02-4f47-aef9-2efff279df71", + "entry_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "payroll_submissions": [ + { + "id": "95a0e70b-fe02-4f47-aef9-2efff279df71", + "account_id": "674744df-9626-47ef-ae2b-4a491be136b5", + "entry_id": "be770ba4-1362-46cd-8c1c-2330ce3a8b69", + "created_at": "2019-05-17T00:00:00.000Z", + "document_external_id": "payslip123456", + "document_filename": "file1-payslip.pdf", + "identity_information": { + "name": "John Smith", + "date_of_birth": "2019-05-17T00:00:00.000Z", + "address": { + "street": "123 Main Street", + "county": "Greater London", + "city": "London", + "post_code": "SW1A 1AA", + "country": "United Kingdom" + }, + "email": "john.smith@company.com", + "phone": 447123456789, + "NI_number": "AB123456C" + }, + "employment_information": { + "employer_name": "Acme Ltd", + "role": "Software Engineer", + "type": "Full-time", + "status": "Active", + "start_date": "2019-05-17T00:00:00.000Z", + "leave_date": "2019-05-17T00:00:00.000Z" + }, + "income_information": { + "pay_date": "2023-05-27T00:00:00.000Z", + "pay_interval_start": "2023-05-01T00:00:00.000Z", + "pay_interval_end": "2023-05-31T00:00:00.000Z", + "pay_frequency": "Monthly", + "earnings": { + "gross_pay": 3500, + "net_pay": 2500, + "base_salary": 3000, + "bonus": 500 + }, + "deductions": { + "income_tax": 500, + "employee_ni": 200, + "employee_pension": 300, + "total_deductions": 1000 + } + } + }, + "fraud_risk": "Low" + ], + "payslip_errors" : [{ + "error" : "File is not a payslip", + "file_name" : "Payslip3.pdf" + }] +} +``` + + + diff --git a/api-reference/introduction.mdx b/api-reference/introduction.mdx deleted file mode 100644 index c835b78b5..000000000 --- a/api-reference/introduction.mdx +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: 'Introduction' -description: 'Example section for showcasing API endpoints' ---- - - - If you're not looking to build API reference documentation, you can delete - this section by removing the api-reference folder. - - -## Welcome - -There are two ways to build API documentation: [OpenAPI](https://mintlify.com/docs/api-playground/openapi/setup) and [MDX components](https://mintlify.com/docs/api-playground/mdx/configuration). For the starter kit, we are using the following OpenAPI specification. - - - View the OpenAPI specification file - - -## Authentication - -All API endpoints are authenticated using Bearer tokens and picked up from the specification file. - -```json -"security": [ - { - "bearerAuth": [] - } -] -``` diff --git a/api-reference/members/create.mdx b/api-reference/members/create.mdx new file mode 100644 index 000000000..99eab8517 --- /dev/null +++ b/api-reference/members/create.mdx @@ -0,0 +1,4 @@ +--- +title: 'Invite a Member' +openapi: 'POST /members' +--- diff --git a/api-reference/members/delete.mdx b/api-reference/members/delete.mdx new file mode 100644 index 000000000..b04015194 --- /dev/null +++ b/api-reference/members/delete.mdx @@ -0,0 +1,4 @@ +--- +title: 'Delete a Member' +openapi: 'DELETE /members/{member_id}' +--- \ No newline at end of file diff --git a/api-reference/members/get.mdx b/api-reference/members/get.mdx new file mode 100644 index 000000000..3d838e711 --- /dev/null +++ b/api-reference/members/get.mdx @@ -0,0 +1,4 @@ +--- +title: 'Retrieve a Member' +openapi: 'GET /members/{member_id}' +--- \ No newline at end of file diff --git a/api-reference/members/list.mdx b/api-reference/members/list.mdx new file mode 100644 index 000000000..de0bdffbc --- /dev/null +++ b/api-reference/members/list.mdx @@ -0,0 +1,4 @@ +--- +title: 'List all Members' +openapi: 'GET /members' +--- diff --git a/api-reference/members/signin.mdx b/api-reference/members/signin.mdx new file mode 100644 index 000000000..d29bf302a --- /dev/null +++ b/api-reference/members/signin.mdx @@ -0,0 +1,4 @@ +--- +title: 'Member Sigin In' +openapi: 'POST /members/signin' +--- diff --git a/api-reference/openapi.json b/api-reference/openapi.json deleted file mode 100644 index b1509be04..000000000 --- a/api-reference/openapi.json +++ /dev/null @@ -1,195 +0,0 @@ -{ - "openapi": "3.0.1", - "info": { - "title": "OpenAPI Plant Store", - "description": "A sample API that uses a plant store as an example to demonstrate features in the OpenAPI specification", - "license": { - "name": "MIT" - }, - "version": "1.0.0" - }, - "servers": [ - { - "url": "http://sandbox.mintlify.com" - } - ], - "security": [ - { - "bearerAuth": [] - } - ], - "paths": { - "/plants": { - "get": { - "description": "Returns all plants from the system that the user has access to", - "parameters": [ - { - "name": "limit", - "in": "query", - "description": "The maximum number of results to return", - "schema": { - "type": "integer", - "format": "int32" - } - } - ], - "responses": { - "200": { - "description": "Plant response", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Plant" - } - } - } - } - }, - "400": { - "description": "Unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - } - }, - "post": { - "description": "Creates a new plant in the store", - "requestBody": { - "description": "Plant to add to the store", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NewPlant" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "plant response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Plant" - } - } - } - }, - "400": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - } - } - }, - "/plants/{id}": { - "delete": { - "description": "Deletes a single plant based on the ID supplied", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "ID of plant to delete", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "responses": { - "204": { - "description": "Plant deleted", - "content": {} - }, - "400": { - "description": "unexpected error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "Plant": { - "required": [ - "name" - ], - "type": "object", - "properties": { - "name": { - "description": "The name of the plant", - "type": "string" - }, - "tag": { - "description": "Tag to specify the type", - "type": "string" - } - } - }, - "NewPlant": { - "allOf": [ - { - "$ref": "#/components/schemas/Plant" - }, - { - "required": [ - "id" - ], - "type": "object", - "properties": { - "id": { - "description": "Identification number of the plant", - "type": "integer", - "format": "int64" - } - } - } - ] - }, - "Error": { - "required": [ - "error", - "message" - ], - "type": "object", - "properties": { - "error": { - "type": "integer", - "format": "int32" - }, - "message": { - "type": "string" - } - } - } - }, - "securitySchemes": { - "bearerAuth": { - "type": "http", - "scheme": "bearer" - } - } - } -} \ No newline at end of file diff --git a/api-reference/openapi.yml b/api-reference/openapi.yml new file mode 100644 index 000000000..1c36f719c --- /dev/null +++ b/api-reference/openapi.yml @@ -0,0 +1,1983 @@ +--- +openapi: 3.0.1 +info: + title: Payroll API + description: A full flagged payroll api provided by Teal + version: 1.0.0 +servers: + - url: https://api.sandbox.goteal.co + description: Sandbox server for experiments + - url: https://api.goteal.co + description: Production server +security: + - ApiKeyAuth: [] + - MemberBearerAuth: [] +paths: + "/users": + description: A User represents the borrowers and end-users that + give consented access and/or upload their payslips to our API. + post: + summary: Creates a user. + requestBody: + required: true + content: + application/json: + schema: + "$ref": "#/components/schemas/User" + responses: + "201": + description: Created + content: + application/json: + schema: + "$ref": "#/components/schemas/CreatedUser" + "400": + "$ref": "#/components/responses/BadRequest" + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + "409": + "$ref": "#/components/responses/ConflictedResource" + + get: + summary: List all users. + parameters: + - name: orederd_by + in: query + description: The field of the user to order by + required: false + schema: + type: string + enum: [name, email, created_at] + default: created_at + example: name + - name: offset + in: query + description: The offset to start at + required: false + schema: + type: integer + format: int32 + minimum: 1 + default: 0 + - name: limit + in: query + description: The number of items to return + required: false + schema: + type: integer + format: int32 + minimum: 1 + maximum: 100 + default: 20 + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + pagination: + "$ref": "#/components/schemas/PaginationWithOrderedBy" + users: + type: array + items: + "$ref": "#/components/schemas/CreatedUser" + "400": + "$ref": "#/components/responses/BadRequest" + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + "/users/{user_id}": + description: A User represents the borrowers and end-users that give + consented access and/or upload their payslips to our API. + get: + summary: Creates a user. + parameters: + - in: path + name: user_id + schema: + type: string + required: true + description: ID of the user to delete. + responses: + "200": + description: REturns the user details + content: + application/json: + schema: + "$ref": "#/components/schemas/CreatedUser" + "400": + "$ref": "#/components/responses/BadRequest" + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + put: + description: Update a user and its config. If a field is sent as null then it is considered an unset or removal. + summary: Updates a user. + parameters: + - in: path + name: user_id + schema: + type: string + required: true + description: ID of the user to update. + requestBody: + required: true + content: + application/json: + schema: + "$ref": "#/components/schemas/User" + responses: + "200": + description: Updated + content: + application/json: + schema: + "$ref": "#/components/schemas/CreatedUser" + "400": + "$ref": "#/components/responses/BadRequest" + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + "404": + "$ref": "#/components/responses/ResourceNotFound" + delete: + summary: Deletes a user along with all the files, accounts, entries and payroll submission associated with it. + parameters: + - in: path + name: user_id + schema: + type: string + required: true + description: ID of the user to delete. + responses: + "204": + description: No Content + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: User deleted successfully. + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + "404": + "$ref": "#/components/responses/ResourceNotFound" + "/users/recurring": + description: List users that are either eligible or ineliegible for recurring paroll checks + get: + parameters: + - name: recurring_enabled + in: query + description: Filter on users that are eligible for recurring checks, and those that are not + required: false + schema: + type: boolean + default: true + - name: offset + in: query + description: The offset to start at + required: false + schema: + type: integer + format: int32 + minimum: 1 + default: 0 + - name: limit + in: query + description: The number of items to return + required: false + schema: + type: integer + format: int32 + minimum: 1 + maximum: 500 + default: 100 + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + pagination: + "$ref": "#/components/schemas/PaginationWithOrderedBy" + users: + type: array + items: + "$ref": "#/components/schemas/CreatedUser" + "400": + "$ref": "#/components/responses/BadRequest" + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + "/user-tokens": + post: + summary: Generate a link token for authentication in subsequent steps to a unique user ID. + requestBody: + required: true + content: + application/json: + schema: + properties: + user_id: + type: string + description: The id of the user + example: 95a0e70b-fe02-4f47-aef9-2efff279df71 + responses: + "201": + description: Created + content: + application/json: + schema: + "$ref": "#/components/schemas/UserToken" + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + "/accounts": + description: + Account are the payroll and HR platform accesses that users link to Teal, + providing a stream of income and employment data + post: + summary: Creates an account for the user e.g. a Xero account + requestBody: + required: true + content: + application/json: + schema: + allOf: + - type: object + required: ["user_id"] + properties: + user_id: + type: string + description: The id of the user + example: 95a0e70b-fe02-4f47-aef9-2efff279df71 + - "$ref": "#/components/schemas/Accounts" + responses: + "201": + description: Created + content: + application/json: + schema: + allOf: + - "$ref": "#/components/schemas/AccountsResult" + - type: object + properties: + authorization_url: + type: string + description: Authrorization url for provider using OAUTH2 + example: https://provider/authenticate?redirect_url=https://api.teal.com/auth&state=state&scope=read + - type: object + properties: + mfa_method: + "$ref": "#/components/schemas/MfaMethod" + "400": + "$ref": "#/components/responses/BadRequest" + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + get: + summary: Returns all accounts for the user (e.g. Xero accounts) + parameters: + - in: query + name: user_id + schema: + type: string + required: true + description: ID of the user to get accounts for + - name: offset + in: query + description: The offset to start at + required: false + schema: + type: integer + format: int32 + minimum: 1 + default: 0 + - name: limit + in: query + description: The number of items to return + required: false + schema: + type: integer + format: int32 + minimum: 1 + maximum: 1000 + default: 500 + responses: + "200": + description: Successfully retrieved accounts + content: + application/json: + schema: + type: object + properties: + pagination: + "$ref": "#/components/schemas/Pagination" + accounts: + type: array + items: + "$ref": "#/components/schemas/AccountsResult" + "400": + "$ref": "#/components/responses/BadRequest" + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + "/accounts/recurring": + description: + Return accounts that are applicable for recurring checks for a given user + get: + summary: Returns all recurring accounts for the user + parameters: + - in: query + name: user_id + schema: + type: string + required: true + description: ID of the user to get accounts for + - name: offset + in: query + description: The offset to start at + required: false + schema: + type: integer + format: int32 + minimum: 1 + default: 0 + - name: limit + in: query + description: The number of items to return + required: false + schema: + type: integer + format: int32 + minimum: 1 + maximum: 1000 + default: 500 + responses: + "200": + description: Successfully retrieved accounts + content: + application/json: + schema: + type: object + properties: + pagination: + "$ref": "#/components/schemas/Pagination" + accounts: + type: array + items: + "$ref": "#/components/schemas/RecurringAccountsResult" + "400": + "$ref": "#/components/responses/BadRequest" + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + + "/accounts/{account_id}": + description: + Account are the payroll and HR platform accesses that users link to Teal, + providing a stream of income and employment data + delete: + summary: Deletes an account for the user (e.g. a Xero account) + parameters: + - in: path + name: account_id + schema: + type: string + required: true + description: ID of the account to delete + responses: + "204": + description: No Content + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Account deleted successfully. + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + "404": + "$ref": "#/components/responses/ResourceNotFound" + get: + summary: Retrieve an account with an account ID + parameters: + - in: path + name: account_id + schema: + type: string + required: true + description: ID of the account to retrieve + responses: + "200": + description: Succesfully retrieved account + content: + application/json: + schema: + "$ref": "#/components/schemas/AccountsResult" + "404": + "$ref": "#/components/responses/ResourceNotFound" + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + + "/entries": + get: + summary: Returns all entries for a user account sorted desc by created_at + parameters: + - in: query + name: user_id + schema: + type: string + required: true + description: ID of the user to get entries for + - name: account_id + in: query + description: The offset to start at + required: false + schema: + type: string + format: uuid + - name: offset + in: query + description: The offset to start at + required: false + schema: + type: integer + format: int32 + minimum: 1 + default: 0 + - name: limit + in: query + description: The number of items to return + required: false + schema: + type: integer + format: int32 + minimum: 1 + maximum: 100 + default: 20 + responses: + "201": + description: Created + content: + application/json: + schema: + type: object + properties: + pagination: + "$ref": "#/components/schemas/Pagination" + entries: + type: array + items: + "$ref": "#/components/schemas/Entry" + "400": + "$ref": "#/components/responses/BadRequest" + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + "/entries/{entry_id}": + delete: + summary: Deletes an entry for the user + parameters: + - in: path + name: entry_id + schema: + type: string + required: true + description: ID of the entry to delete + responses: + "204": + description: No Content + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Entry deleted successfully. + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + "404": + "$ref": "#/components/responses/ResourceNotFound" + "/entries/connections": + description: + For an account, a call to the `/entries/connections` endpoint will query the + connection for data. This endpoint is for adhoc data retrieval for an account; + Teal will also collect the payroll data for users via the WebUI as soon + as a valid connection is established. This endpoint can be applied to any + account, it does not matter if it was established via your own call to + `POST /accounts` or via the Teal WebUI. Note that any new data retrieved via + `/entries/connections` is persisted on Teal's storage and can be retrieved using + the `/payroll/{user_id}` endpoint. + post: + summary: Connects a user account to a payroll provider + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [account_id] + properties: + account_id: + type: string + format: uuid + description: The account id to use to connect to the payroll provider + example: 95a0e70b-fe02-4f47-aef9-2efff279df71 + responses: + "201": + description: Created + content: + application/json: + schema: + "$ref": "#/components/schemas/PayrollData" + "400": + "$ref": "#/components/responses/BadRequest" + "401": + "$ref": "#/components/responses/UnauthorizedBearer" + "/entries/payslips": + post: + summary: Fetches payslips for a user account + description: Uploads payslips as a multipart form data http request. + Every payslip must be a searchable and readable PDF file. + Scanned images as pdf files are not supported. + If a file part header `external_document_id` is provided it + will be returned as `document_external_id` in the respective + payroll submission. If the header is not provided, the form + field name for the file part will be used instead. + security: + - BearerAuth: [] + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + payslip: + type: string + format: binary + description: File to upload + example: > + @"/local/file/system/path/to/payslip/Payslip_44404148.pdf" + responses: + "201": + description: Created + content: + application/json: + schema: + allOf: + - "$ref": "#/components/schemas/PayrollData" + - properties: + payslip_errors: + description: List of errors for payslips that passed valdiation but failed to upload + type: array + items: + type: object + properties: + error: + type: string + file_name: + type: string + "400": + "$ref": "#/components/responses/BadRequest" + "401": + "$ref": "#/components/responses/UnauthorizedBearer" + "/payroll/{user_id}": + get: + summary: Returns all plants from the system that the user has access to + parameters: + - in: path + name: user_id + schema: + type: string + required: true + description: ID of the user to get payroll for + - name: orederd_by + in: query + description: The field of the user to order by + required: false + schema: + type: string + enum: [account_id, entry_id, created_at] + default: created_at + example: name + - name: offset + in: query + description: The offset to start at + required: false + schema: + type: integer + format: int32 + minimum: 1 + default: 0 + - name: limit + in: query + description: The number of items to return + required: false + schema: + type: integer + format: int32 + minimum: 1 + maximum: 100 + default: 20 + - name: account_id + in: query + description: The account_id to fileter on + required: false + schema: + type: string + format: uuid + - name: entry_id + in: query + description: The account_id to fileter on + required: false + schema: + type: string + format: uuid + - name: document_external_id + in: query + description: The document_external_id to fileter on + required: false + schema: + type: string + - name: created_after + in: query + description: Filter out payroll dated before the given date + required: false + schema: + type: string + format: date-time + example: 2019-05-17T00:00:00.000Z + - name: created_before + in: query + description: Filter out payroll dated after the given date + required: false + schema: + type: string + format: date-time + example: 2019-05-17T00:00:00.000Z + responses: + "200": + description: Plant response + content: + application/json: + schema: + type: object + required: [pagination, payroll_submissions] + properties: + pagination: + "$ref": "#/components/schemas/Pagination" + payroll_submissions: + type: array + items: + "$ref": "#/components/schemas/PayrollSubmission" + "400": + description: Unexpected error + content: + application/json: + schema: + "$ref": "#/components/schemas/Error" + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + "/documents/{document_id}": + get: + summary: Returns all documents for a user entry + parameters: + - in: path + name: document_id + schema: + type: string + required: true + description: The id of the file + responses: + "200": + description: Document response + content: + application/json: + schema: + allOf: + - "$ref": "#/components/schemas/DocumentResponse" + - properties: + file_contents: + type: string + description: Base-64 encoded string containing file contents + example: "JVBERi0xLjQKJdPr6eEKMSAwIG9iago8PC9UaXRsZSAoS..." + "400": + "$ref": "#/components/responses/BadRequest" + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + delete: + summary: Deletes a document by id + parameters: + - in: path + name: document_id + schema: + type: string + required: true + description: The id of the file + responses: + "204": + description: No content + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Document [14a542ed-f060-42cc-9b13-dea206cd9837] deleted + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + "404": + "$ref": "#/components/responses/ResourceNotFound" + "/documents": + get: + summary: List all documents for a user. + parameters: + - in: query + name: user_id + schema: + type: string + required: true + description: ID of the user to get documents for + - name: offset + in: query + description: The offset to start at + required: false + schema: + type: integer + format: int32 + minimum: 0 + default: 0 + - name: limit + in: query + description: The number of items to return + required: false + schema: + type: integer + format: int32 + minimum: 1 + maximum: 100 + default: 50 + responses: + "200": + description: Document response + content: + application/json: + schema: + type: object + properties: + pagination: + "$ref": "#/components/schemas/Pagination" + documents: + type: array + items: + allOf: + - "$ref": "#/components/schemas/DocumentResponse" + - properties: + file_contents: + type: string + description: Location to download the document + example: "/documents/a9249254-ab10-4a2d-b709-eda95f5ecd59" + "400": + description: Unexpected error + content: + application/json: + schema: + "$ref": "#/components/schemas/Error" + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + "/webhooks": + post: + summary: Creates a webhook. + requestBody: + required: true + content: + application/json: + schema: + "$ref": "#/components/schemas/Webhook" + responses: + "201": + description: Created + content: + application/json: + schema: + "$ref": "#/components/schemas/CreatedWebhook" + "400": + "$ref": "#/components/responses/BadRequest" + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + get: + summary: Lists all the webhooks + parameters: + - name: orederd_by + in: query + description: The field of the user to order by + required: false + schema: + type: string + enum: [name, url, created_at] + default: created_at + example: name + - name: offset + in: query + description: The offset to start at + required: false + schema: + type: integer + format: int32 + minimum: 1 + default: 0 + - name: limit + in: query + description: The number of items to return + required: false + schema: + type: integer + format: int32 + minimum: 1 + maximum: 100 + default: 20 + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + pagination: + "$ref": "#/components/schemas/Pagination" + webhooks: + type: array + items: + "$ref": "#/components/schemas/CreatedWebhook" + "400": + "$ref": "#/components/responses/BadRequest" + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + "/webhooks/{webhook_id}": + get: + summary: Returns a webhook. + parameters: + - in: path + name: webhook_id + schema: + type: string + required: true + description: ID of the webhook to get + responses: + "201": + description: Created + content: + application/json: + schema: + "$ref": "#/components/schemas/CreatedWebhook" + "400": + "$ref": "#/components/responses/BadRequest" + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + delete: + summary: Deletes a webhook. + parameters: + - in: path + name: webhook_id + schema: + type: string + required: true + description: ID of the webhook to delete + responses: + "204": + description: No Content + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Webhook deleted successfully. + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + "404": + "$ref": "#/components/responses/ResourceNotFound" + "/members": + post: + summary: Invites a member to be part of the team. The member will need to create their account if they don't exist. + requestBody: + required: true + content: + application/json: + schema: + "$ref": "#/components/schemas/InviteMemberRequest" + responses: + "201": + description: Created + content: + application/json: + schema: + "$ref": "#/components/schemas/Member" + "400": + "$ref": "#/components/responses/BadRequest" + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + get: + summary: Lists all the team members including the Owner + parameters: + - name: offset + in: query + description: The offset to start at + required: false + schema: + type: integer + format: int32 + minimum: 1 + default: 0 + - name: limit + in: query + description: The number of items to return + required: false + schema: + type: integer + format: int32 + minimum: 1 + maximum: 100 + default: 20 + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + pagination: + "$ref": "#/components/schemas/Pagination" + members: + type: array + items: + "$ref": "#/components/schemas/Member" + "400": + "$ref": "#/components/responses/BadRequest" + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + "/members/signin": + post: + summary: Sign in a member + security: [] # This declares that no authorization is required for this endpoint + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/MemberSignInRequest" + responses: + "200": + description: Successful signin + content: + application/json: + schema: + $ref: "#/components/schemas/MemberSignInResponse" + "400": + $ref: "#/components/responses/BadRequest" + "/members/{member_id}": + get: + summary: Returns a Member. + parameters: + - in: path + name: member_id + schema: + type: string + required: true + description: ID of the Member to get + responses: + "201": + description: Created + content: + application/json: + schema: + "$ref": "#/components/schemas/Member" + "400": + "$ref": "#/components/responses/BadRequest" + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + delete: + summary: Deletes a Member. + parameters: + - in: path + name: member_id + schema: + type: string + required: true + description: ID of the member to delete + responses: + "204": + description: No Content + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Member deleted successfully. + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + "404": + "$ref": "#/components/responses/ResourceNotFound" + "/api-keys": + description: API keys provide programmatic access to the API. You can have up to 3 active API keys at any time. + get: + summary: List all active API keys. + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + api_keys: + type: array + items: + "$ref": "#/components/schemas/ApiKey" + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + post: + summary: Create a new API key. + requestBody: + required: true + content: + application/json: + schema: + "$ref": "#/components/schemas/ApiKeyCreateRequest" + responses: + "201": + description: Created + content: + application/json: + schema: + "$ref": "#/components/schemas/ApiKeyWithSecret" + "400": + "$ref": "#/components/responses/BadRequest" + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + "403": + description: Forbidden - Maximum number of API keys (3) reached + content: + application/json: + schema: + "$ref": "#/components/schemas/Error" + "/api-keys/{api_key_id}": + description: Manage individual API keys. + delete: + summary: Revoke an API key. + parameters: + - in: path + name: api_key_id + schema: + type: string + format: uuid + required: true + description: ID of the API key to revoke + responses: + "204": + description: No Content - API key successfully revoked + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + "404": + "$ref": "#/components/responses/ResourceNotFound" + "/configuration": + get: + summary: Returns the client configuration + responses: + "200": + description: OK + content: + application/json: + schema: + "$ref": "#/components/schemas/ClientConfiguration" + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" + "404": + "$ref": "#/components/responses/ResourceNotFound" + put: + summary: Updates the client configuration + description: Partial updates supported; omitted fields preserve their existing values. + requestBody: + required: true + content: + application/json: + schema: + "$ref": "#/components/schemas/ClientConfiguration" + responses: + "200": + description: OK + content: + application/json: + schema: + "$ref": "#/components/schemas/ClientConfiguration" + "400": + "$ref": "#/components/responses/BadRequest" + "401": + "$ref": "#/components/responses/UnauthorizedApiKey" +components: + responses: + UnauthorizedApiKey: + description: Unauthorized if the X-API-KEY is not provided or is wrong + content: + application/json: + schema: + "$ref": "#/components/schemas/Unauthorized" + BadRequest: + description: Bad request if one of the required parameters is missing + content: + application/json: + schema: + "$ref": "#/components/schemas/Error" + ConflictedResource: + description: Conflicted resource when creating if that resource already exists + content: + application/json: + schema: + "$ref": "#/components/schemas/Conflict" + UnauthorizedBearer: + description: Unauthorized if the X-API-KEY is not provided or is wrong + content: + application/json: + schema: + "$ref": "#/components/schemas/UnauthorizedBarer" + ResourceNotFound: + description: Resource not found + content: + applcation/json: + schema: + "$ref": "#/components/schemas/NotFound" + schemas: + ApiKey: + type: object + required: [id, name, createdAt] + properties: + id: + type: string + format: uuid + description: Unique identifier for the API key + example: "550e8400-e29b-41d4-a716-446655440000" + name: + type: string + description: Human-readable name for the API key + example: "Backend service key" + secret: + type: string + nullable: true + description: The API key secret. Only returned when creating a key, null for list operations. + example: null + createdAt: + type: string + format: date-time + description: When the API key was created + example: "2019-05-17T00:00:00.000Z" + ApiKeyWithSecret: + type: object + required: [id, name, secret, createdAt] + properties: + id: + type: string + format: uuid + description: Unique identifier for the API key + example: "550e8400-e29b-41d4-a716-446655440000" + name: + type: string + description: Human-readable name for the API key + example: "Backend service key" + secret: + type: string + description: The API key secret. Only shown once at creation time. + example: "a1b2c3d4e5f6g7h8i9j0" + createdAt: + type: string + format: date-time + description: When the API key was created + example: "2019-05-17T00:00:00.000Z" + ApiKeyCreateRequest: + type: object + required: [name] + properties: + name: + type: string + description: Human-readable name for the API key + example: "Backend service key" + Date: + type: string + format: date-time + example: 2019-05-17T00:00:00.000Z + FraudRisk: + type: string + description: The fraud risk level assessment. Fraud risk will be present if the payroll data is coming from a payslip source + enum: [Low, Medium, High] + example: Low + PaginationWithOrderedBy: + type: object + allOf: + - "$ref": "#/components/schemas/Pagination" + required: [offset, limit, count, ordered_by] + properties: + ordered_by: + type: string + example: created_at + Pagination: + type: object + required: [offset, limit, count] + properties: + offset: + description: The offset to start at - zero based + type: integer + format: int32 + minimum: 1 + default: 0 + example: 75 + limit: + type: integer + format: int32 + minimum: 1 + maximum: 100 + default: 20 + example: 25 + count: + type: integer + format: int32 + example: 14 + minimum: 0 + total_count: + type: integer + format: int32 + example: 89 + User: + required: [name, email] + type: object + properties: + name: + description: The full name of the user. It doesn't have to be unique for the client. + type: string + example: John Smith + email: + description: The email of the user. The field does not have to be a real email address. It can be the user's identifier in your system. + type: string + format: email + example: john.smith@company.com + recurring_checks_enabled: + description: Determines if recurring checks are enabled for this user. Overrides the same setting if present in client level configuration. + type: boolean + recurring_check_frequency: + description: Frequency of the recurring check for this user. Overrides the same setting if present in client level configuration. Available frequencies in production are [WEEKLY, DAILY]. Sandbox allows HOURLY frequency in addition. + type: string + example: WEEKLY + recurring_check_end_date: + description: End date of the recurring check schedule. Overrides the same setting if present in client level configuration. + "$ref": "#/components/schemas/Date" + payroll_period_months: + description: Lookback period of the payroll to search on account creation. Overrides the same setting if present in client level configuration. + type: integer + format: int32 + CreatedUser: + allOf: + - required: [user_id, created_at] + properties: + user_id: + type: string + format: uuid + description: Unique identifier for user + created_at: + "$ref": "#/components/schemas/Date" + description: Creation time in UTC of the user + - "$ref": "#/components/schemas/User" + UserToken: + type: object + required: [token, webapp_link, created_at] + properties: + token: + type: string + description: The token to be used in subsequent requests + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiOTVhMGU3MGItZmUwMi00ZjQ3LWFlZjktMmVmZmYyNzlkZjcxIiwiaWF0IjoxNTE2MjM5MDIyfQ.4S5J + webapp_link: + type: string + description: URL of the teal webapp with short version of token. Ignore if not utilising webapp. + example: https://api.sandbox.goteal.co/r/ZwgQkNEMZ4QDkQ + created_at: + "$ref": "#/components/schemas/Date" + ClientConfiguration: + type: object + properties: + payslip_upload_enabled: + type: boolean + description: Determines if payslip upload via documents is allowed as a fallback if there is not connection to payroll providers + example: true + payroll_period_months: + type: integer + format: int32 + description: Lookback period of the payroll to search on account creation + example: 24 + client_logo: + type: string + description: Client logo asset or URL + example: image.png + client_callback_url: + type: string + description: Callback URL for client application. This will be redirected to once the user completes their journey on Teal's platform + example: https://app.client.com/callback + recurring_checks_enabled: + type: boolean + description: Determines if recurring checks are enabled client-wide + example: true + recurring_check_frequency: + type: string + description: Frequency of the recurring check for the client. Available frequencies in production are [WEEKLY, MONTHLY]. Sandbox allows HOURLY frequency in addition. + example: WEEKLY + recurring_check_end_date: + "$ref": "#/components/schemas/Date" + description: End date of the recurring check schedule client-wide + Accounts: + type: object + required: [payroll_provider] + properties: + payroll_provider: + type: string + description: The payroll provider to use + example: quickbooks + user_name: + type: string + description: The login of the user + example: john.smith@company.com + password: + type: string + description: The password of the user + example: very-secret-password + AccountsResult: + type: object + required: [payroll_provider, user_name, created_at, account_id] + properties: + account_id: + type: string + description: The id of the account + example: 95a0e70b-fe02-4f47-aef9-2efff279df71 + payroll_provider: + type: string + description: The payroll provider to use + example: quickbooks + user_name: + type: string + description: The login of the user + example: john.smith@company.com + status: + type: string + description: Status of account + example: PENDING + created_at: + "$ref": "#/components/schemas/Date" + mfa_enabled: + type: boolean + description: Is mfa enabled for this account + latest_pay_date: + description: The latest pay date of the payroll under this account + "$ref": "#/components/schemas/Date" + RecurringAccountsResult: + type: object + required: [payroll_provider, user_name, created_at, account_id, mfa_enabled] + properties: + account_id: + type: string + description: The id of the account + example: 95a0e70b-fe02-4f47-aef9-2efff279df71 + payroll_provider: + type: string + description: The payroll provider to use + example: quickbooks + user_name: + type: string + description: The login of the user + example: john.smith@company.com + created_at: + "$ref": "#/components/schemas/Date" + mfa_enabled: + type: boolean + description: Is mfa enabled for this account + latest_pay_date: + description: The latest pay date of the payroll under this account + "$ref": "#/components/schemas/Date" + next_recurring_check: + description: Date of the next recurring check for this account + "$ref": "#/components/schemas/Date" + recurring_check_end_date: + description: End date of the recurring schedule for this account + "$ref": "#/components/schemas/Date" + recurring_check_frequency: + description: Frequency of the recurring check for this account. Available frequencies [WEEKLY, MONTHLY] + type: string + example: WEEKLY + employee_name: + description: Employee name for which this account is linked to + type: string + example: Amazon + MfaMethod: + type: object + discriminator: + propertyName: type + mapping: + Code: "#/components/schemas/CodeMfa" + Date: "#/components/schemas/DateMfa" + PasswordWithPositions: "#/components/schemas/PasswordWithPositions" + oneOf: + - "$ref": "#/components/schemas/CodeMfa" + - "$ref": "#/components/schemas/DateMfa" + - "$ref": "#/components/schemas/PasswordWithPositions" + CodeMfa: + type: object + required: + - type + - source + properties: + type: + type: string + enum: [ Code ] + source: + type: string + enum: [ SMS, EMAIL, AUTHENTICATOR_APP ] + example: + type: Code + source: AUTHENTICATOR_APP + DateMfa: + type: object + required: + - type + - format + - message + properties: + type: + type: string + enum: [ Date ] + format: + type: string + example: "yyyy-mm-dd" + message: + type: string + example: "Date of birth" + example: + type: Date + format: "yyyy-mm-dd" + message: "Date of birth" + PasswordWithPositions: + type: object + required: + - type + - positions + - message + properties: + type: + type: string + enum: [ PasswordWithPositions ] + positions: + type: array + items: + type: integer + example: [ 1, 2, 3 ] + message: + type: string + example: "Memorable word" + example: + type: PasswordWithPositions + positions: [ 1, 2, 3 ] + message: "Memorable word" + Entry: + type: object + properties: + account_id: + type: string + format: uuid + description: The id of the account + example: 95a0e70b-fe02-4f47-aef9-2efff279df71 + entry_id: + type: string + format: uuid + description: The id of the entry + example: 95a0e70b-fe02-4f47-aef9-2efff279df71 + created_at: + "$ref": "#/components/schemas/Date" + DocumentResponse: + type: object + required: + ["document_id", "file_name", "type", "upload_time", "file_contents"] + properties: + document_id: + type: string + format: uuid + description: A unique document identifier + example: 95a0e70b-fe02-4f47-aef9-2efff279df71 + file_name: + type: string + description: Name of the file + example: payslip1.pdf + document_external_id: + type: string + description: A string coming from the parameters of the + uploaded payslip1.pdf either the form field name + or `external_document_id` header for the file part. + It might not be unique and depends + on what's passed when uploading. + example: payslip123456 + type: + type: string + description: The type of document + example: "Payslip" + upload_time: + "$ref": "#/components/schemas/Date" + PayrollData: + type: object + required: [account_id, entry_id, pagination, payroll_submissions] + properties: + account_id: + type: string + format: uuid + description: The id of the account + example: 95a0e70b-fe02-4f47-aef9-2efff279df71 + entry_id: + type: string + format: uuid + description: The id of the entry + payroll_submissions: + type: array + items: + "$ref": "#/components/schemas/PayrollSubmission" + Webhook: + type: object + required: [name, events, url] + properties: + name: + type: string + description: A unique name of the webhook + example: user-payroll-submitted + events: + type: array + items: + type: string + example: ["user-payroll-submitted", "user-payroll-created"] + url: + type: string + format: url + example: https://webhooks.company.com + signing_secret: + type: string + description: A secret to sign the body of the webhook as described at "https://docs.goteal.co" + writeOnly: true # Not included in response objects + example: very-secret + encryption_key: + type: string + description: A randomly generated 32-byte key, encoded in Base64, used to encrypt messages. + This key is necessary for encryption but is not retrievable after being set. + writeOnly: true + example: St/73LZ1xHkwX5TanL7V+YAVczn4acozPf3cFySKQXI= + config: + type: object + description: Configuration properties of the webhook + writeOnly: true + properties: + include_payload: + type: boolean + description: Whether to include or not the payload of the event in the webhook + example: true + CreatedWebhook: + allOf: + - required: [webhook_id] + properties: + webhook_id: + type: string + description: The id of the webhook + example: 95a0e70b-fe02-4f47-aef9-2efff279df71 + - "$ref": "#/components/schemas/Webhook" + - required: [created_at] + properties: + created_at: + "$ref": "#/components/schemas/Date" + Member: + type: object + required: [email, role, status, mfa_status] + properties: + email: + type: string + format: email + example: john.smith@goteal.co + role: + type: string + description: The Role of the Member + items: + type: string + example: "Owner" + name: + type: string + description: Names of the Member + example: John Smith + status: + type: string + description: the status of the Member + enum: + [ + "unconfirmed", + "confirmed", + "archived", + "compromised", + "unknown", + "reset_required", + "force_change_password", + "external_provider", + ] + readOnly: true + example: "unconfirmed" + mfa_status: + type: string + description: the status of the Member + enum: ["sms", "email", "disabled"] + readOnly: true + example: "sms" + InviteMemberRequest: + type: object + required: [email, role] + properties: + email: + type: string + format: email + example: john.smith@goteal.co + role: + type: string + enum: [FullMember] + description: The Role of the Member + items: + type: string + example: FullMember + Error: + required: + - errors + type: object + properties: + errors: + type: array + items: + type: string + description: An array of messages describing the errors + example: ["The request is missing the required field `name`"] + MemberSignInRequest: + type: object + required: + - username + - password + properties: + username: + type: string + description: The username of the member + example: john.doe@example.com + password: + type: string + description: The password of the member + format: password + example: mySecurePassword123 + MemberSignInResponse: + type: object + required: + - jwt + - member + properties: + jwt: + type: string + description: JSON Web Token for authentication + example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + member: + $ref: '#/components/schemas/MemberResponse' + MemberResponse: + type: object + required: + - id + - email + - mfa_status + properties: + id: + type: string + description: Unique identifier for the member + example: "123e4567-e89b-12d3-a456-426614174000" + email: + type: string + format: email + description: Email address of the member + example: "john.doe@example.com" + role: + type: string + description: Role of the member + example: "Admin" + name: + type: string + description: Name of the member + example: "John Doe" + status: + type: string + description: Status of the member + example: "active" + mfa_status: + type: string + description: Multi-factor authentication status of the member + example: "sms" + Unauthorized: + type: object + properties: + errors: + type: string + description: An array of messages describing the errors + example: ["No X-API-KEY header provided or wrong value"] + UnauthorizedBarer: + type: object + properties: + errors: + type: array + items: + type: string + description: An array of messages describing the errors + example: ["Not Authorization: Bearer provided or wrong value"] + Conflict: + type: object + properties: + errors: + type: array + items: + type: string + description: An array of messages describing the conflict errors + example: + [ + "This user already exists. [name] and [email] must be a unique combination", + ] + NotFound: + type: object + properties: + errors: + type: string + description: An array of messages describing the not found errors + example: + [ + "The resource with id [8be3bb60-5ab4-4bd2-90e6-3b015691a827] does not exist", + ] + PayrollSubmission: + type: object + required: + - id + - account_id + - entry_id + - created_at + - identity_information + - employment_information + - income_information + properties: + id: + type: string + format: uuid + example: 95a0e70b-fe02-4f47-aef9-2efff279df71 + account_id: + type: string + format: uuid + description: The id of the account + example: 674744df-9626-47ef-ae2b-4a491be136b5 + entry_id: + type: string + format: uuid + description: The id of the entry + example: be770ba4-1362-46cd-8c1c-2330ce3a8b69 + created_at: + "$ref": "#/components/schemas/Date" + description: The date the payroll was submitted + example: 2019-05-17T00:00:00.000Z + document_id: + type: string + nullable: true + format: uuid + description: The id of the document + example: 95a0e70b-fe02-4f47-aef9-2efff279df71 + document_external_id: + type: string + nullable: true + description: A string coming from the parameters of the + uploaded payslip1.pdf either the form field name + or `external_document_id` header for the file part. + It might not be unique and depends + on what's passed when uploading. + If the payroll submission doesn't originate from + a payslip this will be null. + example: payslip123456 + document_filename: + type: string + nullable: true + description: If the payroll submission originates from a payslip + this will be the file name of the uploaded payslip. + It will be duplicate if same file names are uploaded + If the payroll submission doesn't originate from + a payslip this will be null. + example: file1-payslip.pdf + identity_information: + $ref: "#/components/schemas/IdentityInformation" + employment_information: + $ref: "#/components/schemas/EmploymentInformation" + income_information: + $ref: "#/components/schemas/IncomeInformation" + fraud_risk: + "$ref": "#/components/schemas/FraudRisk" + nullable: true + IdentityInformation: + type: object + properties: + name: + type: string + description: The full name of the user + example: John Smith + date_of_birth: + "$ref": "#/components/schemas/Date" + address: + $ref: "#/components/schemas/Address" + email: + type: string + description: The email of the user + format: email + example: john.smith@company.com + phone: + type: string + description: The phone number of the user + example: +447123456789 + NI_number: + type: string + description: The national insurance number of the user + example: AB123456C + Address: + type: object + properties: + street: + type: string + description: The street of the address + example: 123 Main Street + county: + type: string + description: The county of the address + example: Greater London + city: + type: string + description: The city of the address + example: London + post_code: + type: string + description: The post code of the address + example: SW1A 1AA + country: + type: string + description: The country of the address + example: United Kingdom + EmploymentInformation: + type: object + properties: + employer_name: + type: string + description: The name of the employer + example: Acme Ltd + role: + type: string + description: The role of the user + example: Software Engineer + type: + type: string + description: The type of employment + example: Full-time + status: + type: string + description: > + The status of the employment. Status `active` will be used when the user is employed in the company, + alternatively `inactive` will be used when the user is not employed in the company anymore. + enum: + - active + - inactive + example: active + start_date: + "$ref": "#/components/schemas/Date" + leave_date: + "$ref": "#/components/schemas/Date" + nullable: true + IncomeInformation: + type: object + properties: + pay_date: + "$ref": "#/components/schemas/Date" + type: string + description: The date the payroll was submitted + example: 2023-05-27 + pay_interval_start: + "$ref": "#/components/schemas/Date" + type: string + description: The start date of the pay interval + example: 2023-05-01 + pay_interval_end: + "$ref": "#/components/schemas/Date" + type: string + description: The end date of the pay interval + example: 2023-05-31 + pay_frequency: + type: string + description: The frequency of the pay + example: Monthly + earnings: + $ref: "#/components/schemas/Earnings" + deductions: + $ref: "#/components/schemas/Deductions" + Earnings: + type: object + properties: + gross_pay: + type: number + description: The gross pay + example: 3500 + net_pay: + type: number + description: The net pay + example: 2500 + base_salary: + type: number + description: The base salary + example: 3000 + bonus: + type: number + description: The bonus + example: 500 + Deductions: + type: object + properties: + income_tax: + type: number + description: The income tax + example: 500 + employee_ni: + type: number + description: The employee national insurance + example: 200 + employee_pension: + type: number + description: The employee pension + example: 300 + total_deductions: + type: number + description: The total deductions + example: 1000 + + securitySchemes: + ApiKeyAuth: + type: apiKey + in: header + name: X-API-KEY + MemberBearerAuth: + type: http + scheme: bearer + description: Bearer token for authentication. The token should be the one returned by the "/members/signin" + BearerAuth: + type: http + scheme: bearer + description: Bearer token for authentication. The token should be the one returned by the /user-tokens endpoint. \ No newline at end of file diff --git a/api-reference/payroll/get.mdx b/api-reference/payroll/get.mdx new file mode 100644 index 000000000..35fff4e21 --- /dev/null +++ b/api-reference/payroll/get.mdx @@ -0,0 +1,4 @@ +--- +title: 'Get Payroll' +openapi: 'GET /payroll/{user_id}' +--- diff --git a/api-reference/quickstart.mdx b/api-reference/quickstart.mdx new file mode 100644 index 000000000..b4b92e058 --- /dev/null +++ b/api-reference/quickstart.mdx @@ -0,0 +1,142 @@ +--- +title: 'Quickstart' +description: 'Welcome to the comprehensive guide to use Teal API, a pivotal resource for accessing real-time payroll and employment verification data.' +--- + +## Overview + +Teal is a payroll API platform, streamlining the entire income verification process by connecting you to data sourced from payroll and HR systems or analysing income documents, all with the user consent at the core. + +Teal supports you during the underwriting process, increasing both speed and accuracy levels. With this information you can assess the level of affordability of a borrower, by verifying their level of income and their employment status. + + +# Authentication + +Authentication is a critical aspect of securing access to our API, ensuring that only authorized users can perform operations. We employ API key authentication, a method chosen for its simplicity and effectiveness in safeguarding our services. Upon registration or on request, users are provided with a unique API key via email, which must be included in all API requests. + +## Acquiring an API Key + +Upon successful registration or request, an API key is issued to each client application through a secure email. This key is essential for accessing the API and must be safeguarded. + +### Key Features: + +- **Uniqueness**: Each API key is unique, tied to a client application. +- **Revocable**: API keys can be revoked and regenerated for security. +- **Confidentiality**: Keep your API key confidential to prevent unauthorized use. + +## Making Authorized Requests + +Include the API key in the request headers to make authorized calls to our API. + +### Required Headers: + +- `Content-Type: application/json`: Indicates the request body is JSON formatted. +- `X-API-KEY: `: Your issued API key. + +### Example Request: + +```bash +curl --request GET \ + --url https://api.sandbox.goteal.co/endpoint \ + --header 'Content-Type: application/json' \ + --header 'X-API-KEY: ' +``` + +Authorization via API key is essential for secure access to our API. Include the necessary headers in your requests to utilize our API's features safely. + +## Environments + +Our API offers two environments to cater to different stages of development and production: + +- **Production:** For real-world use. Base URL: `https://api.goteal.co/v1` +- **Sandbox:** For testing with simulated data. Base URL: `https://api.sandbox.goteal.co/v1` + +# Testing Tools and Execution Methods + +Our API documentation supports various testing and execution methods to suit different development workflows, emphasizing flexibility and accessibility. We provide examples using cURL commands for their universality, but the principles apply across all execution methods. + + +## Execution Methods: + +### 1. Documentation Web Client + +- **Interactivity**: Execute API calls directly within the browser through our interactive documentation, ideal for immediate testing without setup. +- **User-Friendly**: Pre-filled requests in a simple interface make it accessible for newcomers to explore API functionalities quickly. + +**How to use:** + +1. Navigate to an endpoint definition. Example: [Create a user](https://docs.goteal.co/api-reference/users/create) +2. Fill in the required API key in the `X-API-KEY` field of the Authorization dropdown. +3. Fill in the required parameters of the Body dropdown. +4. Click "Send". +5. The response will be displayed below the request form. + +### 2. Postman + +- **Advanced Features:** Postman offers advanced testing capabilities, including environment variables and test scripts, ideal for complex API workflows. +- **Try it out**: Import our API collection to Postman for a seamless transition from our documentation to your development environment. + +**How to use:** + +- It is required to have a workspace to import the collection. If you don't have one, you can create one following the [instructions](https://learning.postman.com/docs/getting-started/first-steps/creating-your-first-workspace/). + + +
+
+ +**Tip**: + +Remember to set the environment variables in Postman. You can find the environment variables in the top right corner of the Postman interface. + +Postman Environment + + +### 3. Terminal with cURL + +- **Flexibility**: cURL commands, used for direct API interactions from the command line, support scripting and task automation. +- **Compatibility**: With broad OS support, cURL is a reliable choice for endpoint testing, demonstrated by clear, adaptable examples in our documentation. + +### 4. Programmatically + +- **Integration-Ready**: Implement API calls in your application code for automated testing or development, facilitating continuous integration workflows. +- **Language Agnostic**: Any programming language capable of making HTTP requests can interact with our API, enabling integration into diverse development environments. + +## Technical Considerations: + +- **HTTP Methods**: Understand the HTTP methods (GET, POST, DELETE, etc.) our API uses, as they dictate how to interact with each endpoint. +- **Request Headers**: Pay close attention to required headers, especially `Content-Type` and `Authorization`, as they're crucial for successful API calls. +- **Error Handling**: Familiarize yourself with our API's error responses to effectively handle and debug issues during development. + +In summary, whether you prefer testing APIs directly from our documentation, using command-line tools like cURL, or integrating API calls into your software, our documentation provides the necessary guidance. The provided cURL examples offer a solid foundation for understanding how to craft requests, applicable across different technologies and platforms. + + + +## Data + +After connecting a new account, data is retrieved in stages: + +- **Instant Data:** Quickly available post-connection, covering static information that changes infrequently. +- **Event Data:** Compiled over time, providing a historical view that can extend back several years. + +We continuously update and enrich your data as new information becomes available. + + +### Security + +To protect your API keys: + +- **HTTPS Security**: Always use HTTPS to ensure your API requests are encrypted, protecting your API key and data from eavesdropping. +- **Key Management**: Treat your API key as a sensitive password—regenerate it periodically and immediately replace it if compromised. +- **Secure API Key Storage**: Store your API key securely, avoiding exposure in client-side code or publicly accessible areas. + +### Handling Responses + +Our API uses standard HTTP status codes for feedback: + +- `200 OK`: Request successful. +- `201 Created`: New resource created. +- `204 No Content`: Successfully processed, no content to return. +- `400 Bad Request`: Invalid request format. +- `401 Unauthorized`: Authentication required. + + diff --git a/api-reference/users/create.mdx b/api-reference/users/create.mdx new file mode 100644 index 000000000..391bf0a2c --- /dev/null +++ b/api-reference/users/create.mdx @@ -0,0 +1,4 @@ +--- +title: 'Create a User' +openapi: 'POST /users' +--- diff --git a/api-reference/users/delete.mdx b/api-reference/users/delete.mdx new file mode 100644 index 000000000..b2d1e8dbf --- /dev/null +++ b/api-reference/users/delete.mdx @@ -0,0 +1,4 @@ +--- +title: 'Delete a user' +openapi: 'DELETE /users/{user_id}' +--- \ No newline at end of file diff --git a/api-reference/users/get.mdx b/api-reference/users/get.mdx new file mode 100644 index 000000000..69f4d064c --- /dev/null +++ b/api-reference/users/get.mdx @@ -0,0 +1,4 @@ +--- +title: 'Retrieve a user' +openapi: 'GET /users/{user_id}' +--- \ No newline at end of file diff --git a/api-reference/users/list-recurring.mdx b/api-reference/users/list-recurring.mdx new file mode 100644 index 000000000..6fb32a915 --- /dev/null +++ b/api-reference/users/list-recurring.mdx @@ -0,0 +1,4 @@ +--- +title: 'List all recurring users' +openapi: 'GET /users/recurring' +--- \ No newline at end of file diff --git a/api-reference/users/list.mdx b/api-reference/users/list.mdx new file mode 100644 index 000000000..af9821bec --- /dev/null +++ b/api-reference/users/list.mdx @@ -0,0 +1,4 @@ +--- +title: 'List all users' +openapi: 'GET /users' +--- \ No newline at end of file diff --git a/api-reference/users/tokens.mdx b/api-reference/users/tokens.mdx new file mode 100644 index 000000000..fed8f9c1d --- /dev/null +++ b/api-reference/users/tokens.mdx @@ -0,0 +1,4 @@ +--- +title: 'Generate User token' +openapi: 'POST /user-tokens' +--- diff --git a/api-reference/users/update.mdx b/api-reference/users/update.mdx new file mode 100644 index 000000000..473fdf5da --- /dev/null +++ b/api-reference/users/update.mdx @@ -0,0 +1,4 @@ +--- +title: 'Update a User' +openapi: 'PUT /users/{user_id}' +--- \ No newline at end of file diff --git a/api-reference/webhooks.mdx b/api-reference/webhooks.mdx new file mode 100644 index 000000000..bb557a728 --- /dev/null +++ b/api-reference/webhooks.mdx @@ -0,0 +1,522 @@ +--- +title: 'Webhooks' +description: 'Stay updated with real-time notifications via our Webhooks service.' +--- + +## Overview +Webhooks instantly inform your application about significant events, such as payroll submissions or updates, without the need for polling. + +### Use Cases: +- Alerting when users submit or create payroll data. This use case can be handled by subscribing for event of type `user-payroll-submitted` +- Notifying upon successful account linkages. Not yet supported as event type. +- Triggering actions after data updates. Not yet supported as event type. + +Webhooks are system-wide meaning subscriptions are not limited to specific users or accounts. For example, if any user updates their payroll with the `user-payroll-submitted` subscription enabled, a webbook will be delivered with the respective user information enclosed. + +### Quick Reference: +- **Webhooks List**: Detailed descriptions of all events you can subscribe to. +- **Setup Guide**: Instructions for subscribing and managing webhooks. + +### Supported Event: +- `user-payroll-submitted` - notifies when any user creates an entry with new payroll data. + Includes the payroll data entry as json if `include_payload` is set in the configuration. +- No more events are supported at the moment. + +## Setting Up Webhooks + +Create a webhook by sending a POST request. Specify which events to monitor and where to send notifications. + +```bash +curl --request POST \ + --url https://api.sandbox.goteal.co/webhooks \ + --header 'Content-Type: application/json' \ + --header 'X-API-KEY: ' \ + --data '{ + "events": ["user-payroll-submitted", "user-payroll-created"], + "name": "user-payroll-submitted", + "signing_secret": "very-secret", + "encryption_key": "St/73LZ1xHkwX5TanL7V+YAVczn4acozPf3cFySKQXI=", + "url": "https://webhooks.site" + }' +``` +- **events**: Choose events like `user-payroll-submitted` for targeted notifications. +- **name**: Label your webhook for easier management. +- **url**: Your endpoint to receive webhook payloads. +- **signing_secret**: (Optional) A secret for secure webhook verification. +- **encryption_key**: (Optional) A Base64-encoded 32-byte key used to encrypt webhook payloads. + +### Verifying Webhooks +If the optional `signing_secret` was provided in the webhook subscription, it can be used to validate incoming webhook +signatures, ensuring data integrity and authenticity. + +When encryption is enabled, the message is first encrypted and then signed. +Therefore, the signature should be verified against the encrypted message, +not the decrypted content. + +To verify if a received webhook event is authentic: +- Encode the webook payload with the `signing_secret` using `HMAC-SHA512`. You can then check that the result of this encoding matches the contents in the +`X-Teal-Signature` header. If it matches then the webhook received is genuine. Example verification: + +If your `signing_secret` was `mysecret` and you received the following webhook payload: + +```json +{ + "event": "user-payroll-submitted", + "user_id": "4708334c-70b3-437d-8e46-91ff5c9a8d7d", + "timestamp": "2024-04-04T12:00:00.00Z" +} +``` + +The signature to check against would be: + +``` +"X-Teal-Signature" : "a30540779107a19069257432b775b74b16b32214616638fae2e6027a41a3f2dfb08f44daf3862c335d08fb83501fc769f73d49a1cb137f96f31c6a7db412c197" +``` + +Example of verifying signature: + + +```javascript JS +const crypto = require('crypto'); + +// The signature as "X-Teal-Signature" header +const receivedTealSignature = '...'; +const client_secret = 'my little secret'; +const requestBody = '...'; // Your request body here + +// Make sure to use the raw requestBody when validating the signature. +var signature = crypto + .createHmac('sha512', client_secret) + .update(requestBody) + .digest('hex'); + +if (receivedTealSignature === signature) { + console.log('Signature is valid'); +} else { + console.log('Signature is invalid'); +} +``` + +```python Python +import hmac +import hashlib + +receivedTealSignature = '...' # The signature as "X-Teal-Signature" header +client_secret = 'my little secret' +requestBody = '...' # Your request body here + +# Make sure to use the raw requestBody when validating the signature. +signature = hmac.new( + client_secret.encode(), + msg=requestBody.encode(), + digestmod=hashlib.sha512 +).hexdigest() + +if receivedTealSignature == signature: + print('Signature is valid') +else: + print('Signature is invalid') +``` + +```java Java +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +public class Main { + public static void main(String[] args) throws Exception { + + // The signature as "X-Teal-Signature" header + String receivedTealSignature = "..."; + String client_secret = "my little secret"; + String requestBody = "..."; // Your request body here + + // Make sure to use the raw requestBody when validating the signature. + Mac sha512_HMAC = Mac.getInstance("HmacSHA512"); + SecretKeySpec secret_key = new SecretKeySpec( + client_secret.getBytes(StandardCharsets.UTF_8), + "HmacSHA512" + ); + sha512_HMAC.init(secret_key); + + byte[] hash = sha512_HMAC + .doFinal(requestBody.getBytes(StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(hash.length * 2); + for(byte b : hash) { + sb.append(String.format("%02x", b)); + } + String signature = sb.toString(); + + if (receivedTealSignature.equals(signature)) { + System.out.println("Signature is valid"); + } else { + System.out.println("Signature is invalid"); + } + } +} +``` + +```kotlin Kotlin +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec +import java.nio.charset.StandardCharsets +import java.security.InvalidKeyException +import java.security.NoSuchAlgorithmException + + // The signature as "X-Teal-Signature" header +val receivedTealSignature = "..." +val client_secret = "my little secret" +val requestBody = "..." // Your request body here + +// Make sure to use the raw requestBody when validating the signature. +val sha512_HMAC = Mac.getInstance("HmacSHA512") +val secret_key = SecretKeySpec( + client_secret.toByteArray(StandardCharsets.UTF_8), + "HmacSHA512" +) +sha512_HMAC.init(secret_key) + +val hash = sha512_HMAC + .doFinal(requestBody.toByteArray(StandardCharsets.UTF_8)) +val signature = hash.joinToString("") { "%02x".format(it) } + +if (receivedTealSignature == signature) { + println("Signature is valid") +} else { + println("Signature is invalid") +} + +``` + + + +### Generating Encryption Keys +If you want to use encrypted webhooks, you'll need to provide a 32-byte encryption key encoded in Base64 when creating your webhook. Here's how to generate a secure random key in different languages: + + + +```javascript JS +const crypto = require('crypto'); + +// Generate a random 32-byte key +const encryptionKey = crypto.randomBytes(32); + +// Encode the key as Base64 for use with the API +const encryptionKeyBase64 = encryptionKey.toString('base64'); +console.log('Encryption Key:', encryptionKeyBase64); +// Example output: St/73LZ1xHkwX5TanL7V+YAVczn4acozPf3cFySKQXI= +``` + +```python Python +import os +import base64 + +# Generate a random 32-byte key +encryption_key = os.urandom(32) + +# Encode the key as Base64 for use with the API +encryption_key_base64 = base64.b64encode(encryption_key).decode('utf-8') +print(f"Encryption Key: {encryption_key_base64}") +# Example output: St/73LZ1xHkwX5TanL7V+YAVczn4acozPf3cFySKQXI= +``` + +```java Java +import java.security.SecureRandom; +import java.util.Base64; + +public class GenerateEncryptionKey { + public static void main(String[] args) { + // Generate a random 32-byte key + SecureRandom secureRandom = new SecureRandom(); + byte[] key = new byte[32]; + secureRandom.nextBytes(key); + + // Encode the key as Base64 for use with the API + String encryptionKeyBase64 = Base64.getEncoder().encodeToString(key); + System.out.println("Encryption Key: " + encryptionKeyBase64); + // Example output: St/73LZ1xHkwX5TanL7V+YAVczn4acozPf3cFySKQXI= + } +} +``` + +```kotlin Kotlin +import java.security.SecureRandom +import java.util.Base64 + +// Generate a random 32-byte key +val secureRandom = SecureRandom() +val key = ByteArray(32) +secureRandom.nextBytes(key) + +// Encode the key as Base64 for use with the API +val encryptionKeyBase64 = Base64.getEncoder().encodeToString(key) +println("Encryption Key: $encryptionKeyBase64") +// Example output: St/73LZ1xHkwX5TanL7V+YAVczn4acozPf3cFySKQXI= +``` + + + +### Decrypting Webhook Payloads +When you provide an `encryption_key`, the webhook payload will be encrypted using AES/GCM/NoPadding. To decrypt: + +1. The encrypted payload is sent in the request body +2. The initialization vector is provided in the `X-Teal-Encryption-IV` header (Base64 encoded) + +Here's how to decrypt the payload in different languages: + + + +```javascript JS +const crypto = require('crypto'); + +function decrypt( + encryptedBase64, + ivBase64, + encryptionKeyBase64 +) { + const encryptionKey = Buffer.from(encryptionKeyBase64, 'base64'); + const iv = Buffer.from(ivBase64, 'base64'); + const encryptedBuffer = Buffer.from(encryptedBase64, 'base64'); + + // Extract the auth tag (last 16 bytes) and ciphertext + const authTag = encryptedBuffer.subarray(encryptedBuffer.length - 16); + const ciphertext = encryptedBuffer.subarray(0, encryptedBuffer.length - 16); + + const decipher = crypto.createDecipheriv('aes-256-gcm', encryptionKey, iv); + decipher.setAuthTag(authTag); + let decrypted = decipher.update(ciphertext); + try { + const final = decipher.final(); + if (final.length > 0) { + decrypted = Buffer.concat([decrypted, final]); + } + + const payload = JSON.parse(decrypted.toString('utf8')); + console.log('Decrypted payload:', payload); + } catch (error) { + console.error('Decryption failed:', error); + } +} + +decrypt( + webhookRequest.body, + webhookRequest.headers['x-teal-encryption-iv'], + yourSecretStorage.base64EncryptionKey +) +``` + +```python Python +import base64 +import json +from Crypto.Cipher import AES + +def decrypt( + encrypted_base64: str, + iv_base64: str, + encryption_key_base64: str +) -> dict: + encryption_key = base64.b64decode(encryption_key_base64) + print(f"Key length: {len(encryption_key)} bytes") + iv = base64.b64decode(iv_base64) + print(f"IV length: {len(iv)} bytes") + + encrypted_data = base64.b64decode(encrypted_base64) + print(f"Encrypted data length: {len(encrypted_data)} bytes") + + auth_tag = encrypted_data[-16:] + ciphertext = encrypted_data[:-16] + print(f"Auth tag length: {len(auth_tag)} bytes") + print(f"Ciphertext length: {len(ciphertext)} bytes") + + cipher = AES.new(encryption_key, AES.MODE_GCM, nonce=iv) + + decrypted_data = cipher.decrypt_and_verify(ciphertext, auth_tag) + + payload = json.loads(decrypted_data.decode('utf-8')) + print("Decryption successful!") + print(f"Decrypted payload: {payload}") + return payload + +if __name__ == "__main__": + decrypt( + webhookRequest.body, + webhookRequest.headers["x-teal-encryption-iv"], + secretStorage.base64Secret + ) +``` + +```java Java +import javax.crypto.Cipher; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.util.Base64; + +public class DecryptMessage { + + public static String decryptWebhookPayload( + String encryptedData, + String encryptionKey, + String ivBase64 + ) throws Exception { + byte[] key = Base64.getDecoder().decode(encryptionKey); + byte[] iv = Base64.getDecoder().decode(ivBase64); + byte[] encryptedBytes = Base64.getDecoder().decode(encryptedData); + + SecretKeySpec secretKey = new SecretKeySpec(key, "AES"); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + GCMParameterSpec spec = new GCMParameterSpec(128, iv); + + cipher.init(Cipher.DECRYPT_MODE, secretKey, spec); + byte[] decryptedBytes = cipher.doFinal(encryptedBytes); + + return new String(decryptedBytes, "UTF-8"); + } + + public static void main(String[] args) { + try { + String result = decryptWebhookPayload( + "nBPqEB4T1g/88q1P2qm7oYH5MYbR/NpCVBCytMZsB7ux4xR+OYPS8Y/Ye8giQb6UplDitbKlFlHPoA6pzMu3CmD2cew4JvluyKiOQBE61OwETwJ++KkvwuB9WxegKnhncWqg6qwn9WkRGtmKpfrxiXrdvj7V4aKSOFkO8ByWYztQp58Ublm3yNPQyQmnzJOR+/4v8OMbVivcAkumRu8PO2EYr2uqfPvNMonXiNE5fnqJieZKFvRnDi+s6TNZpSsCzWPgfDGn+EqiDbjqWw+eWnUYd3BQ75OOJficovrzk1dYkONm5uYTnZYSdiwMzNm7", + "7/PxZATSzWbQkS8ZjSt0f+bTMt9oQ6jkm0aYFe3NN24=", + "vdBbe9Tk7aqo1pZi" + ); + System.out.println("Decrypted payload: " + result); + } catch (Exception e) { + System.err.println("Decryption failed: " + e.getMessage()); + e.printStackTrace(); + } + } +} +``` + +```kotlin Kotlin +import java.util.Base64 +import javax.crypto.Cipher +import javax.crypto.spec.GCMParameterSpec +import javax.crypto.spec.SecretKeySpec + +fun decryptWebhookPayload( + encryptedData: String, + encryptionKey: String, + ivBase64: String +): String { + val key = Base64.getDecoder().decode(encryptionKey) + val iv = Base64.getDecoder().decode(ivBase64) + val encryptedBytes = Base64.getDecoder().decode(encryptedData) + val secretKey = SecretKeySpec(key, "AES") + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + val spec = GCMParameterSpec(128, iv) + cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) + val decryptedBytes = cipher.doFinal(encryptedBytes) + return String(decryptedBytes, Charsets.UTF_8) +} + +fun main() { + try { + val result = decryptWebhookPayload( + "nBPqEB4T1g/88q1P2qm7oYH5MYbR/NpCVBCytMZsB7ux4xR+OYPS8Y/Ye8giQb6UplDitbKlFlHPoA6pzMu3CmD2cew4JvluyKiOQBE61OwETwJ++KkvwuB9WxegKnhncWqg6qwn9WkRGtmKpfrxiXrdvj7V4aKSOFkO8ByWYztQp58Ublm3yNPQyQmnzJOR+/4v8OMbVivcAkumRu8PO2EYr2uqfPvNMonXiNE5fnqJieZKFvRnDi+s6TNZpSsCzWPgfDGn+EqiDbjqWw+eWnUYd3BQ75OOJficovrzk1dYkONm5uYTnZYSdiwMzNm7", + "7/PxZATSzWbQkS8ZjSt0f+bTMt9oQ6jkm0aYFe3NN24=", + "vdBbe9Tk7aqo1pZi" + ) + println("Decrypted payload: $result") + } catch (e: Exception) { + println("Decryption failed: ${e.message}") + e.printStackTrace() + } +} +``` + + +## Webhook Management + +### Viewing subscriptions +List your current webhooks with a simple GET request to our webhook endpoint `GET /webhooks`. + +### Deleting subscriptions +Unsubscribe using the webhook ID in the request `DELETE /webhooks/{webhook_id}` + +## Best Practices + +- **Secure Endpoints**: Ensure your webhook URL is HTTPS-secured to safeguard transmitted data. +- **Manage Secrets**: Keep your `signing_secret` and `encryption_key` confidential. +- **Use Encryption**: Consider using the encryption feature when your team doesn't control HTTPS termination and you want payloads to be decrypted only by applications possessing the encryption key. As an example, this could help prevent sensitive or GDPR-regulated information from being logged in systems not designed to handle such data (there might be more cases for your specific situation). +- **Alternative to Encryption**: As an alternative to encryption, consider disabling the payload in webhooks by not setting `include_payload` in your webhook configuration. +- **Regular Review**: Periodically check your webhook subscriptions and adjust as needed to match your application's requirements. + +Leverage our webhooks to make your application more dynamic and responsive to changes without the overhead of continuous polling. + + +## Webhook Example + +Example of webhook request body configured to include the payload by setting +```"include_payload": "true"``` in POST ``` /webhooks``` request body. + +If the payroll data comes from uploading a payslip the `account_id` +whould be `null` but `document_external_id` and `document_filename` would be populated + + +is related to uploading a payslip. +```json +{ + "event": "user-payroll-submitted", + "user_id": "e9b7676b-f107-429c-8d17-cf3fd1362fd3", + "timestamp": "2024-04-17T13:56:03.277Z", + "entry_id": "cc48a12c-0e98-47aa-b281-f203403470d0", + "account_id": "f68961c9-a083-4bfd-a3c6-0ae169b48f35", + "payload": { + "entry_id": "cc48a12c-0e98-47aa-b281-f203403470d0", + "account_id": "f68961c9-a083-4bfd-a3c6-0ae169b48f35", + "payroll_submissions": [ + { + "id": "0187c66e-e7e5-811c-b006-2232f00f426a", + "account_id": "f68961c9-a083-4bfd-a3c6-0ae169b48f35", + "entry_id": "cc48a12c-0e98-47aa-b281-f203403470d0", + "created_at": "2024-04-17T13:56:03.118Z", + "document_external_id": null, + "document_filename": null, + "identity_information": { + "name": "Jane Doe", + "date_of_birth": "1990-04-01", + "address": { + "street": "123 Main St", + "county": "Anyshire", + "city": "Anycity", + "post_code": "AB12 3CD", + "country": "Countryland" + }, + "email": "jane.doe@example.com", + "phone": "+1234567890", + "NI_number": "AB123456C", + "employment_information": { + "employer_name": "Acme Corp", + "role": "Software Developer", + "type": "Full-Time", + "status": "Active", + "start_date": "2018-06-01", + "leave_date": null + }, + "income_information": { + "pay_date": "2024-02-15", + "pay_interval_start": "2024-02-01", + "pay_interval_end": "2024-02-15", + "pay_frequency": "weekly", + "earnings": { + "gross_pay": 2000, + "net_pay": 1500, + "base_salary": 1900, + "bonus": 100 + }, + "deductions": { + "income_tax": 300, + "employee_ni": 150, + "employee_pension": 50, + "total_deductions": 500 + } + } + } + } + ] + } +} +``` diff --git a/api-reference/webhooks/create.mdx b/api-reference/webhooks/create.mdx new file mode 100644 index 000000000..9bda649f1 --- /dev/null +++ b/api-reference/webhooks/create.mdx @@ -0,0 +1,4 @@ +--- +title: 'Create a Webhook' +openapi: 'POST /webhooks' +--- diff --git a/api-reference/webhooks/delete.mdx b/api-reference/webhooks/delete.mdx new file mode 100644 index 000000000..b26dc107f --- /dev/null +++ b/api-reference/webhooks/delete.mdx @@ -0,0 +1,4 @@ +--- +title: 'Delete a Webhook' +openapi: 'DELETE /webhooks/{webhook_id}' +--- \ No newline at end of file diff --git a/api-reference/webhooks/get.mdx b/api-reference/webhooks/get.mdx new file mode 100644 index 000000000..c119f3d84 --- /dev/null +++ b/api-reference/webhooks/get.mdx @@ -0,0 +1,4 @@ +--- +title: 'Retrieve a Webhook' +openapi: 'GET /webhooks/{webhook_id}' +--- \ No newline at end of file diff --git a/api-reference/webhooks/list.mdx b/api-reference/webhooks/list.mdx new file mode 100644 index 000000000..528b8979e --- /dev/null +++ b/api-reference/webhooks/list.mdx @@ -0,0 +1,4 @@ +--- +title: 'List all webhooks' +openapi: 'GET /webhooks' +--- diff --git a/data.mdx b/data.mdx new file mode 100644 index 000000000..d54c43714 --- /dev/null +++ b/data.mdx @@ -0,0 +1,193 @@ +--- +title: 'Data' +description: 'Manage your data easily and securely' +--- +This documentation is designed to provide developers with detailed insights into our API's structure, focusing on the management, delivery, and security of financial data. + +## 1. Endpoints + +Our API organizes financial data into several key entities, each tailored for specific aspects of data handling and retrieval. + +### Users `/users` + +Represents the borrowers and end-users that give consented access and upload their payslips to our API. + +**Example:** Retrieve a new user +```bash +curl --request GET \ + --url https://api.sandbox.goteal.co/users/{user_id} \ + --header 'X-API-KEY: ' +``` + +A user is made up of their identity information only. + +### Accounts `/accounts` + +Accounts represent user-linked connections to payroll and HR platforms, which provide Teal with a stream of income and employment data. + +**Example:** Link a payroll account +```bash +curl --request POST \ + --url https://api.sandbox.goteal.co/accounts \ + --header 'Content-Type: application/json' \ + --header 'X-API-KEY: ' \ + --data '{ + "user_id": "95a0e70b-fe02-4f47-aef9-2efff279df71", + "password": "very-secret-password", + "payroll_provider": "quickbooks", + "user_name": "john.smith@company.com" +}' +``` + +A user can have multiple accounts to the same provider depending on the type of connection. The `POST /accounts` endpoint is a way to initialize a connection to a provider for a given user; Teal offers a separate provider flow for users via the WebUI as documented in [Introduction](./introduction.mdx). +The status of a account determines if it is able to recieve data, needs authorisation or if it is invalid or expired. An account connected via OAuth will not require credentials to be passed into the account creation request. Any credentials that are passed to Teal are encrypted securely. An account creation +will do an initial payroll retrieval using the optional dates supplied; if none are sent with the request we then check for the client specific duration for which to lookback - if this is not configured, we take a default of 36 months. + +### Entries `/entries` + +Entries log each data submission, tracking the origin (account connection or manual upload) and content type of the data. + +**Example:** Query and persist data for a valid account connection +```bash +curl --request POST \ + --url https://api.sandbox.goteal.co/entries/connections \ + --header 'Authorization: Bearer ' \ + --header 'Content-Type: application/json' \ + --data '{ + "account_id": "95a0e70b-fe02-4f47-aef9-2efff279df71" + }' +``` + +For an account, a call to the `/entries/connections` endpoint will query the connection for data. This endpoint is for adhoc data retrieval for an account; Teal will also collect the payroll data for users via the WebUI as soon +as a valid connection is established. This endpoint can be applied to any account, it does not matter if it was established via your own call to `POST /accounts` or via the Teal WebUI. Note that any new data retrieved via `/entries/connections` is persisted on Teal's storage and can be retrieved using the `/payroll/{user_id}` endpoint. +When querying a connection, new payroll will be fetched based on the initial period used in account creation; Teal uses the most recent `pay_date` of an account to update the connection with the latest payroll seen after this date. + +A manual upload of payslip data using is not linked to an account, but the JSON response of `/entries/payslips` will be in the same format and the payroll data +can be retrieved in the exact same way. The files for upload must be in PDF format. + +### Payroll `/payroll` + +Aggregates detailed payroll information obtained through account synchronization or document analysis, including comprehensive earnings and deductions data. + +**Example:** Access payroll data +```bash +curl --request GET \ + --url https://api.sandbox.goteal.co/payroll/{user_id} \ + --header 'X-API-KEY: '' +``` + +The structure of the returned data will contain fields on employment, identity and income. + +### Documents `/documents` + +Original documents (e.g., payslips) uploaded by users are stored securely, serving as the basis for data extraction and verification. + +**Example:** Retrieve a raw payslip +```bash +curl --request GET \ + --url https://api.sandbox.goteal.co/documents/{document_id} \ + --header 'X-API-KEY: ' +``` + +The response data of the above document request will be JSON containing the Base-64 encoded file contents. This can be decoded and transformed back into PDF format. + +### Configuration `/configuration` + +Configuration represents client-wide settings that control behavior across all users and accounts. + +**Example:** Update client configuration +```bash +curl --request PUT \ + --url https://api.sandbox.goteal.co/configuration \ + --header 'Content-Type: application/json' \ + --header 'X-API-KEY: ' \ + --data '{ + "payslip_upload_enabled": true, + "payroll_period_months": 24, + "client_logo": "https://cdn.client.com/logo.png", + "client_callback_url": "https://app.client.com/callback", + "recurring_checks_enabled": true, + "recurring_check_frequency": "MONTHLY", + "recurring_check_end_date": "2025-01-10T00:00Z" +}' +``` + +Use GET `/configuration` to retrieve the current client configuration. Use PUT `/configuration` to create or update configuration; updates are partial—omitted fields keep their existing values. Validation rules: `recurring_check_frequency` must be one of [WEEKLY, MONTHLY]; `recurring_check_end_date` must be a valid ISO8601 UTC datetime. Some user-level settings (see Users API) can override these client-level defaults. + +## 2. Data Delivery + +Data delivery mechanisms are designed to ensure timely, secure, and efficient access to financial data, facilitating seamless integration and real-time processing. + +### Timing + +Data processing is immediate for connected accounts, with payroll information updated in real-time as new data becomes available. Manual uploads are processed within seconds of submission, ensuring prompt data availability. + +### Availability + +Our API provides round-the-clock access to processed data, supporting queries for real-time and historical financial information. Data is accessible through dedicated endpoints, with responses designed for easy integration into client applications. + +### Webhooks + +Webhooks offer a proactive approach to data delivery, notifying client applications of specific events, such as new data submissions or updates to existing entries, facilitating immediate action or processing. + +**Example:** Configure a webhook for new payroll data +```bash +curl --request POST \ + --url https://api.sandbox.goteal.co/webhooks \ + --header 'Content-Type: application/json' \ + --header 'X-API-KEY: ' \ + --data '{ + "events": [ + "user-payroll-submitted", + "user-payroll-created" + ], + "name": "user-payroll-submitted", + "secret": "very-secret", + "url": "https://webhooks.company.com" +}' +``` + +## 3. Data Security + +Adhering to strict security protocols, our API ensures the integrity and confidentiality of sensitive financial data throughout its lifecycle. + +### Data Sharing + +Access to data through our API is regulated by user permissions alongside OAuth 2.0 protocols, allowing us to securely access user-authorized data. Upon obtaining this data, we then facilitate its secure transfer to the third party requesting this information. This process ensures that data sharing is both compliant with user consent and adheres to stringent privacy standards, safeguarding data integrity during transmission. + +### Data Deletion Requests + +In compliance with privacy laws, our API supports explicit requests for data deletion, allowing our clients to manage the data footprint of their customers if needed be. + +**Example:** Request data deletion +```bash +curl --request DELETE \ + --url https://api.sandbox.goteal.co/users/{user_id} \ + --header 'X-API-KEY: ' +``` + +A deletion of a user deletes all sub-resources that the user is linked to. This includes accounts, payroll and uploaded documents. Once deleted no data can be retreived for that user, nor can any new data be attributed. On an account level, any connection can be revoked by the user at any time. +A deletion of an account removes all entries and payroll associated with it. After deletion, this account can not be retireved nor any data queried from it. + +### Date handling + +All dates in our API are formatted according to [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601). + +Response dates are always returned in UTC, representing the equivalent moment in time regardless of the original time zone offset. For example, 2PM in Berlin: +`2024-02-17T14:00:00+02:00` is returned as `2024-02-17T12:00:00Z` + +When submitting dates to the API, you can include offsets, and these will be respected in all date calculations. + +When integrating with payroll providers that supply dates without time components (e.g., `2024-02-17`), we standardize these to noon UTC (`2024-02-17T12:00:00Z`) to avoid any ambiguity with date boundaries across time zones. +This should be taken into consideration when submitting your own dates for filtering payroll (such as in `POST /accounts`). + +### Client callback + +You can configure a client callback that will be redirected to after the end user has completed their journey. This allows the user to finish their flow on your chosen domain. For successful account connections we will redirect with the +`account_id`, `user_id` and `provider` in the query parameters. For payslip uploads we will redirect with the `user_id` and a `message` parameter. On failure we'll attach an `error` parameter alongside the `user_id`. + +### Security and Compliance + +Our infrastructure employs advanced security measures, including data encryption, regular security audits, and compliance with global data protection standards (e.g., GDPR, CCPA), to safeguard user data against unauthorized access or breaches. + +This enhanced documentation provides a clearer, more detailed view of how our API manages, delivers, and secures financial data, emphasizing our commitment to data integrity, security, and user control. \ No newline at end of file diff --git a/development.mdx b/development.mdx deleted file mode 100644 index 878300893..000000000 --- a/development.mdx +++ /dev/null @@ -1,98 +0,0 @@ ---- -title: 'Development' -description: 'Learn how to preview changes locally' ---- - - - **Prerequisite** You should have installed Node.js (version 18.10.0 or - higher). - - -Step 1. Install Mintlify on your OS: - - - -```bash npm -npm i -g mintlify -``` - -```bash yarn -yarn global add mintlify -``` - - - -Step 2. Go to the docs are located (where you can find `mint.json`) and run the following command: - -```bash -mintlify dev -``` - -The documentation website is now available at `http://localhost:3000`. - -### Custom Ports - -Mintlify uses port 3000 by default. You can use the `--port` flag to customize the port Mintlify runs on. For example, use this command to run in port 3333: - -```bash -mintlify dev --port 3333 -``` - -You will see an error like this if you try to run Mintlify in a port that's already taken: - -```md -Error: listen EADDRINUSE: address already in use :::3000 -``` - -## Mintlify Versions - -Each CLI is linked to a specific version of Mintlify. Please update the CLI if your local website looks different than production. - - - -```bash npm -npm i -g mintlify@latest -``` - -```bash yarn -yarn global upgrade mintlify -``` - - - -## Deployment - - - Unlimited editors available under the [Startup - Plan](https://mintlify.com/pricing) - - -You should see the following if the deploy successfully went through: - - - - - -## Troubleshooting - -Here's how to solve some common problems when working with the CLI. - - - - Update to Node v18. Run `mintlify install` and try again. - - -Go to the `C:/Users/Username/.mintlify/` directory and remove the `mint` -folder. Then Open the Git Bash in this location and run `git clone -https://github.com/mintlify/mint.git`. - -Repeat step 3. - - - - Try navigating to the root of your device and delete the ~/.mintlify folder. - Then run `mintlify dev` again. - - - -Curious about what changed in a CLI version? [Check out the CLI changelog.](/changelog/command-line) diff --git a/docs.json b/docs.json new file mode 100644 index 000000000..41678c576 --- /dev/null +++ b/docs.json @@ -0,0 +1,152 @@ +{ + "$schema": "https://mintlify.com/docs.json", + "theme": "mint", + "name": "Starter Kit", + "colors": { + "primary": "#0D9373", + "light": "#07C983", + "dark": "#0D9373" + }, + "favicon": "/favicon.svg", + "navigation": { + "tabs": [ + { + "tab": "Documentation", + "groups": [ + { + "group": "Get Started", + "pages": [ + "introduction", + "data", + "sandbox", + "recurring-checks" + ] + } + ] + }, + { + "tab": "API Reference", + "groups": [ + { + "group": "API Documentation", + "pages": [ + "api-reference/quickstart", + "api-reference/webhooks" + ] + }, + { + "group": "Users", + "pages": [ + "api-reference/users/create", + "api-reference/users/get", + "api-reference/users/update", + "api-reference/users/list", + "api-reference/users/list-recurring", + "api-reference/users/delete" + ] + }, + { + "group": "User Token", + "pages": [ + "api-reference/users/tokens" + ] + }, + { + "group": "API Keys", + "pages": [ + "api-reference/api-keys/create", + "api-reference/api-keys/list", + "api-reference/api-keys/delete" + ] + }, + { + "group": "Accounts", + "pages": [ + "api-reference/accounts/create", + "api-reference/accounts/list", + "api-reference/accounts/list-recurring", + "api-reference/accounts/get", + "api-reference/accounts/delete" + ] + }, + { + "group": "Entries", + "pages": [ + "api-reference/entries/connections-create", + "api-reference/entries/payslips-create", + "api-reference/entries/list", + "api-reference/entries/delete" + ] + }, + { + "group": "Payroll", + "pages": [ + "api-reference/payroll/get" + ] + }, + { + "group": "Documents", + "pages": [ + "api-reference/documents/get", + "api-reference/documents/list", + "api-reference/documents/delete" + ] + }, + { + "group": "Configuration", + "pages": [ + "api-reference/configuration/get", + "api-reference/configuration/update" + ] + }, + { + "group": "Webhooks", + "pages": [ + "api-reference/webhooks/create", + "api-reference/webhooks/get", + "api-reference/webhooks/list", + "api-reference/webhooks/delete" + ] + }, + { + "group": "Members", + "pages": [ + "api-reference/members/create", + "api-reference/members/get", + "api-reference/members/list", + "api-reference/members/delete", + "api-reference/members/signin" + ] + } + ] + } + ], + "global": { + "anchors": [ + { + "anchor": "Website", + "href": "https://goteal.co", + "icon": "book-open-cover" + } + ] + } + }, + "logo": { + "light": "/logo/light.svg", + "dark": "/logo/dark.svg" + }, + "navbar": { + "links": [ + { + "label": "Support", + "href": "mailto:hello@goteal.co" + } + ] + }, + "footer": { + "socials": { + "website": "https://goteal.co", + "linkedin": "https://www.linkedin.com/company/goteal" + } + } +} \ No newline at end of file diff --git a/favicon.svg b/favicon.svg index 6a3233265..b938b216a 100644 --- a/favicon.svg +++ b/favicon.svg @@ -1,49 +1,17 @@ - - - - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + diff --git a/generator/python/Dockerfile b/generator/python/Dockerfile new file mode 100644 index 000000000..1289d3516 --- /dev/null +++ b/generator/python/Dockerfile @@ -0,0 +1,36 @@ +FROM nikolaik/python-nodejs:latest + +WORKDIR /usr/src/app + +# Install java (needed for openapi gen cli) +RUN apt-get update && \ + DEBIAN_FRONTEND=noninteractive \ + apt-get -y install default-jre-headless && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Install Vim +RUN apt-get update && \ + apt-get install -y vim && \ + rm -rf /var/lib/apt/lists/* + +# Install OpenAPI Generator +RUN npm install @openapitools/openapi-generator-cli -g + +# Copy the OpenAPI definition +COPY api-reference/openapi.yml /usr/src/app/api-reference/ + +# Generate Python client code +RUN openapi-generator-cli generate -i api-reference/openapi.yml -g python -o /tmp/generator/python + +# Setup Python virtual environment +RUN python3 -m venv /tmp/generator/python/venv && \ + . /tmp/generator/python/venv/bin/activate && \ + pip3 install --upgrade pip && \ + pip3 install -r /tmp/generator/python/requirements.txt + +# Copy your Python script to the container +COPY generator/python/populate_sandbox_for_client.py /tmp/generator/python + +# Set the default command to open a bash shell in the directory /tmp/generator/python +CMD ["bash", "-c", "cd /tmp/generator/python && . venv/bin/activate && exec bash"] \ No newline at end of file diff --git a/generator/python/README.md b/generator/python/README.md new file mode 100644 index 000000000..6ef84a961 --- /dev/null +++ b/generator/python/README.md @@ -0,0 +1,14 @@ +# Populate sandbox values for a customer using openapi generator + +1. Navigate to the teal docs root +2. Runing the following to create a container that generates the python client and models from our `openapi.yml` + + `docker build -f generator/python/Dockerfile -t python-populate-sandbox .` + +3. Run following command to put you on the docker container in the generated directory: + + `docker run -it python-populate-sandbox` + +4. Change `client_api_key` and `client_name` in `populate_sandbox_for_client.py` using `vim`. Alter the host depending on needs. + +5. Run `python3 populate_sandbox_for_client.py`. The created identifiers will be printed in the response. \ No newline at end of file diff --git a/generator/python/populate_sandbox_for_client.py b/generator/python/populate_sandbox_for_client.py new file mode 100644 index 000000000..259cbb826 --- /dev/null +++ b/generator/python/populate_sandbox_for_client.py @@ -0,0 +1,117 @@ +import openapi_client +import random +from openapi_client.rest import ApiException +from pprint import pprint + +from openapi_client.models import accounts_post_request, entries_connections_post_request, user_tokens_post_request +from openapi_client.models.accounts_post_request import AccountsPostRequest +from openapi_client.models.entries_connections_post_request import EntriesConnectionsPostRequest +from openapi_client.models.user_tokens_post_request import UserTokensPostRequest +from openapi_client.models.accounts import Accounts +from openapi_client.models.user import User + + +def create_api_client(api_key, api_host): + # Defining the host is optional and defaults to https://api.sandbox.goteal.co + # See configuration.py for a list of all supported configuration parameters. + configuration = openapi_client.Configuration( + host = api_host + ) + + # Configure API key authorization: ApiKeyAuth + configuration.api_key['ApiKeyAuth'] = api_key + + # Enter a context with an instance of the API client + with openapi_client.ApiClient(configuration) as api_client: + # Create an instance of the API class + return openapi_client.DefaultApi(api_client) + +def create_bearer_client(api_host, token): + configuration = openapi_client.Configuration( + host = api_host, + access_token=token + ) + + # Enter a context with an instance of the API client + with openapi_client.ApiClient(configuration) as api_client: + # Create an instance of the API class + return openapi_client.DefaultApi(api_client) + +def create_user(api_instance: openapi_client.DefaultApi, client_name, index): + first_names=('John','Andy','Joe','Dave','Sam','Hannah','Charlotte') + last_names=('Johnson','Smith','Williams','Brown','Church') + + name = random.choice(first_names)+" "+random.choice(last_names) + email = f"{name.lower().replace(' ', '')}{index}@{client_name}.com" + + try: + # Deletes an account for the user (e.g. a Xero account) + api_response = api_instance.users_post(User(name=name, email=email)) + + return api_response.user_id, email + except ApiException as e: + print("Exception when calling creater user: %s\n" % e) + raise + + +def create_account(api_instance: openapi_client.DefaultApi, user_id, user_name): + payroll_provider = "adp" + try: + request = AccountsPostRequest(user_id=user_id, + payroll_provider=payroll_provider, + user_name=user_name) + api_response = api_instance.accounts_post(accounts_post_request=request) + + return api_response.account_id + except ApiException as e: + print("Exception when calling create account: %s\n" % e) + raise + + +def create_entry_for_account(api_instance: openapi_client.DefaultApi, + account_id, user_token): + try: + request = EntriesConnectionsPostRequest(account_id = account_id) + api_response = api_instance.entries_connections_post(request) + + return api_response.entry_id + except ApiException as e: + print("Exception when calling create entry for a connection: %s\n" % e) + raise + + +def create_user_token(api_instance: openapi_client.DefaultApi, user_id): + try: + request = UserTokensPostRequest(user_id=user_id) + api_response = api_instance.user_tokens_post(request) + return api_response.token + except ApiException as e: + print("Exception when creating user token: %s\n" % e) + raise + + +host = "http://host.docker.internal:8080/api" # host machine +#host = "https://api.sandbox.goteal.co" +client_api_key = "" +client_name = "" + +api_instance = create_api_client(client_api_key, host) + +for x in range(15): + print("User {}".format(x)) + user_id, user_name = create_user(api_instance, client_name, x) + account_id = create_account(api_instance, user_id, user_name) + + user_token = create_user_token(api_instance, user_id) + bearer_client = create_bearer_client(host, user_token) + + entry_one_id = create_entry_for_account(bearer_client, account_id, user_token) + entry_two_id = create_entry_for_account(bearer_client, account_id, user_token) + + identifiers = f""" + user_id = {user_id} + account_id = {account_id} + entry_one_id = {entry_one_id} + entry_two_id = {entry_two_id} + """ + print(identifiers) \ No newline at end of file diff --git a/images/hero-dark.svg b/images/hero-dark.svg deleted file mode 100644 index c6a30e88b..000000000 --- a/images/hero-dark.svg +++ /dev/null @@ -1,161 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/images/hero-light.svg b/images/hero-light.svg deleted file mode 100644 index 297d68fb9..000000000 --- a/images/hero-light.svg +++ /dev/null @@ -1,155 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/images/how-it-works.svg b/images/how-it-works.svg new file mode 100644 index 000000000..6c8c12138 --- /dev/null +++ b/images/how-it-works.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/oauth2-example-login.png b/images/oauth2-example-login.png new file mode 100644 index 000000000..d77f23172 Binary files /dev/null and b/images/oauth2-example-login.png differ diff --git a/images/payroll-connection-flow.png b/images/payroll-connection-flow.png new file mode 100644 index 000000000..4a6123909 Binary files /dev/null and b/images/payroll-connection-flow.png differ diff --git a/images/payslip-extraction-flow.png b/images/payslip-extraction-flow.png new file mode 100644 index 000000000..42b62e2fb Binary files /dev/null and b/images/payslip-extraction-flow.png differ diff --git a/images/postman/guide-environment.jpg b/images/postman/guide-environment.jpg new file mode 100644 index 000000000..732f0ae7c Binary files /dev/null and b/images/postman/guide-environment.jpg differ diff --git a/images/webapp/payroll-connection.gif b/images/webapp/payroll-connection.gif new file mode 100644 index 000000000..ed247cfe7 Binary files /dev/null and b/images/webapp/payroll-connection.gif differ diff --git a/images/webapp/payslip-submission.gif b/images/webapp/payslip-submission.gif new file mode 100644 index 000000000..8a7d48366 Binary files /dev/null and b/images/webapp/payslip-submission.gif differ diff --git a/introduction.mdx b/introduction.mdx index 2589c0b93..cce29aecc 100644 --- a/introduction.mdx +++ b/introduction.mdx @@ -1,71 +1,100 @@ --- title: Introduction -description: 'Welcome to the home of your new documentation' +description: 'Welcome to Teal' --- +## How it works +Our modular API gives you access to one of the richest sources of data to improve your underwriting process - payroll data. This data is acquired with user consent either via payroll-linked access or with payslip upload. + Hero Light + +### Connecting payroll account + +By connecting to our API you will have access to consent based access to a user’s historical payroll data. Teal's API enables lenders to receive income data safely from their users, fully embedded within lending flows. + +The user will be walked through a permission flow, where they will allow to connect to their payroll provider. Teal will extract their data, and then expose via API to your services. + Hero Dark -## Setting up +### Uploading payslips -The first step to world-class documentation is setting up your editing environments. +Our API also supports the upload of payslip documents. Thus, enabling the automatic extraction of payslips information and instant data share. We use state of the art LLMs in combination with our own models for maximum accuracy of the extracted data. + +Borrowers will be able to upload their payslips in a webflow hosted by Teal. The payslips will be parsed and data such as identity, income and employment information will be extracted, encrypted and securely stored in the database. The documents are verified for authenticity, controlling for visual inconsistencies and changes in the meta data. + +Upload Payslip FLow - - - Get your docs set up locally for easy development - - - Preview your changes before you push to make sure they're perfect - - +## Accessing user data -## Make it yours +The data is securely exposed via our API. By default, the data is extracted in JSON format after the request is done. The data will be exposed via our API, so lenders can receive it immediately and take a decision based on the shared data. -Update your docs to your brand and add valuable content for the best user conversion. +Example of payroll submission retrieval: - - - Customize your docs to your company's colors and brands - - - Automatically generate endpoints from an OpenAPI spec - - - Build interactive features and designs to guide your users - - - Check out our showcase of our favorite documentation - - +``````JSON +{ + "pagination": { + "count": 14, + "limit": 25, + "offset": 75, + "total_count": 89 + }, + "payroll_submissions": [ + { + "id": "95a0e70b-fe02-4f47-aef9-2efff279df71", + "account_id": "e920bcba-85bb-4a8d-9ce2-0b29e5217e4d", + "entry_id": "f630296a-3046-46ea-86e5-689f08085f1b", + "created_at": "2019-05-17T00:00:00.000Z", + "document_external_id": null, + "document_filename": null, + "employment_information": { + "employer_name": "Acme Ltd", + "leave_date": "2019-05-17T00:00:00.000Z", + "role": "Software Engineer", + "start_date": "2019-05-17T00:00:00.000Z", + "status": "Active", + "type": "Full-time" + }, + "identity_information": { + "NI_number": "AB123456C", + "address": { + "city": "London", + "country": "United Kingdom", + "county": "Greater London", + "post_code": "SW1A 1AA", + "street": "123 Main Street" + }, + "date_of_birth": "2019-05-17T00:00:00.000Z", + "email": "john.smith@company.com", + "name": "John Smith", + "phone": 447123456789 + }, + "income_information": { + "deductions": { + "employee_ni": 200, + "employee_pension": 300, + "income_tax": 500, + "total_deductions": 1000 + }, + "earnings": { + "base_salary": 3000, + "bonus": 500, + "gross_pay": 3500, + "net_pay": 2500 + }, + "pay_date": "2019-05-17T00:00:00.000Z", + "pay_frequency": "Monthly", + "pay_interval_end": "2019-05-17T00:00:00.000Z", + "pay_interval_start": "2019-05-17T00:00:00.000Z" + } + } + ] +} diff --git a/logo/dark.svg b/logo/dark.svg index a6283786c..6f0dce4eb 100644 --- a/logo/dark.svg +++ b/logo/dark.svg @@ -1,55 +1,17 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + diff --git a/logo/light.svg b/logo/light.svg index 582b3b95f..e203ac723 100644 --- a/logo/light.svg +++ b/logo/light.svg @@ -1,51 +1,17 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + diff --git a/mint.json b/mint.json deleted file mode 100644 index f660496b7..000000000 --- a/mint.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "$schema": "https://mintlify.com/schema.json", - "name": "Starter Kit", - "logo": { - "dark": "/logo/dark.svg", - "light": "/logo/light.svg" - }, - "favicon": "/favicon.svg", - "colors": { - "primary": "#0D9373", - "light": "#07C983", - "dark": "#0D9373", - "anchors": { - "from": "#0D9373", - "to": "#07C983" - } - }, - "topbarLinks": [ - { - "name": "Support", - "url": "mailto:hi@mintlify.com" - } - ], - "topbarCtaButton": { - "name": "Dashboard", - "url": "https://dashboard.mintlify.com" - }, - "tabs": [ - { - "name": "API Reference", - "url": "api-reference" - } - ], - "anchors": [ - { - "name": "Documentation", - "icon": "book-open-cover", - "url": "https://mintlify.com/docs" - }, - { - "name": "Community", - "icon": "slack", - "url": "https://mintlify.com/community" - }, - { - "name": "Blog", - "icon": "newspaper", - "url": "https://mintlify.com/blog" - } - ], - "navigation": [ - { - "group": "Get Started", - "pages": [ - "introduction", - "quickstart", - "development" - ] - }, - { - "group": "Essentials", - "pages": [ - "essentials/markdown", - "essentials/code", - "essentials/images", - "essentials/settings", - "essentials/navigation", - "essentials/reusable-snippets" - ] - }, - { - "group": "API Documentation", - "pages": [ - "api-reference/introduction" - ] - }, - { - "group": "Endpoint Examples", - "pages": [ - "api-reference/endpoint/get", - "api-reference/endpoint/create", - "api-reference/endpoint/delete" - ] - } - ], - "footerSocials": { - "twitter": "https://twitter.com/mintlify", - "github": "https://github.com/mintlify", - "linkedin": "https://www.linkedin.com/company/mintsearch" - } -} \ No newline at end of file diff --git a/quickstart.mdx b/quickstart.mdx deleted file mode 100644 index d7f348678..000000000 --- a/quickstart.mdx +++ /dev/null @@ -1,86 +0,0 @@ ---- -title: 'Quickstart' -description: 'Start building awesome documentation in under 5 minutes' ---- - -## Setup your development - -Learn how to update your docs locally and and deploy them to the public. - -### Edit and preview - - - - During the onboarding process, we created a repository on your Github with - your docs content. You can find this repository on our - [dashboard](https://dashboard.mintlify.com). To clone the repository - locally, follow these - [instructions](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) - in your terminal. - - - Previewing helps you make sure your changes look as intended. We built a - command line interface to render these changes locally. 1. Install the - [Mintlify CLI](https://www.npmjs.com/package/mintlify) to preview the - documentation changes locally with this command: ``` npm i -g mintlify ``` - 2. Run the following command at the root of your documentation (where - `mint.json` is): ``` mintlify dev ``` - - - -### Deploy your changes - - - - - Our Github app automatically deploys your changes to your docs site, so you - don't need to manage deployments yourself. You can find the link to install on - your [dashboard](https://dashboard.mintlify.com). Once the bot has been - successfully installed, there should be a check mark next to the commit hash - of the repo. - - - [Commit and push your changes to - Git](https://docs.github.com/en/get-started/using-git/pushing-commits-to-a-remote-repository#about-git-push) - for your changes to update in your docs site. If you push and don't see that - the Github app successfully deployed your changes, you can also manually - update your docs through our [dashboard](https://dashboard.mintlify.com). - - - - -## Update your docs - -Add content directly in your files with MDX syntax and React components. You can use any of our components, or even build your own. - - - - - Add flair to your docs with personalized branding. - - - - Implement your OpenAPI spec and enable API user interaction. - - - - Draw insights from user interactions with your documentation. - - - - Keep your docs on your own website's subdomain. - - - diff --git a/recurring-checks.mdx b/recurring-checks.mdx new file mode 100644 index 000000000..f88130f3a --- /dev/null +++ b/recurring-checks.mdx @@ -0,0 +1,49 @@ +--- +title: 'Recurring Payroll' +description: 'Learn about and configure recurring payroll' +--- + +## Introduction + +To keep your users' payroll up to date without having to manually query each connection, Recurring Payroll can be enabled for your users. This allows you to specify a time frequency for automated payroll fetches. + +The valid frequencies are: +- `WEEKLY` - once a week on Monday 12AM UTC +- `MONTHLY` - once a month on the first day of the month 12AM UTC + +## Execution + +When a recurring check is executed, all of your eligible users will have their accounts updated with their latest payroll. The payroll is queried from the payroll provider by using the most recent `pay_date` of the account, +retrieving the new data that falls after this. Recurring checks can be enabled for all users or for individual users. This also applies to the recurring check configuration where the frequency and end date of the recurring +check can be applied on the user level or to the client level. If a user has a configuration for a given field, it will always take precedence over the client level configuration. If the user has a partial configuration, +then the fields that are not configured will be taken from the client configuration if present. + +## Eligibility + +A users account is eligible for a recurring payroll check if the following are all true: +- The account has `Authorised` status +- Multi-factor authentication is disabled for the account +- The account is active + +To see which users are eligible for recurring checks you can call the `GET /users/recurring` endpoint. For a user to be eligible for the recurring check they need to have at least one account that matches +the above criteria. An account is considered active if the payroll provider has not marked it as inactive and the latest `pay_date` for the account is not older than 40 days. + +A duplicate account is defined as multiple accounts with the same username and payroll provider combination for a given user. If there are duplicate accounts then Teal will take the one with the most recent activity (time of last fetch of payroll), or the +by the creation time (most recently created). A user can have multiple active accounts with different providers and all of these accounts will be part of the recurring update. + +To see which accounts would be part of the recurring schedule for a given user you can call the `GET /accounts/recurring` endpoint. + +## Failures + +If a fetch fails for an account during the recurring payroll check, then it will be placed on a daily retry schedule until it is successful. The daily retry runs at 3AM UTC. When the failure is due to invalid credentials, expired refresh token or a detection of Multi-Factor authentication, these accounts +are excluded from future checks. + +## Sandbox + +In the sandbox environment, recurring checks can be enabled but all the data that is returned for the accounts is mocked up. For testing purposes, the frequency of the recurring payroll can be set to `HOURLY` in this environment if you choose. + +## Activation + +To activate recurring payroll checks please contact Teal with the following information: +- Your chosen frequency +- An optional end date for when you want recurring checks to end. If omitted, the checks will run indefinitely. \ No newline at end of file diff --git a/sandbox.mdx b/sandbox.mdx new file mode 100644 index 000000000..f4e0ca493 --- /dev/null +++ b/sandbox.mdx @@ -0,0 +1,521 @@ +--- +title: 'Sandbox' +description: 'Learn how to preview changes locally' +--- +## Introduction + +The sandbox environment functions as a simulated version of the actual system, designed for testing and development +purposes without interacting with real data. + +This environment employs mock payroll providers and payslip readers in order to allow developers to evaluate +the API interface without compromising real data privacy. + +## Setting Up for Testing + +Before you begin testing, ensure you have the following: +- An API key for authentication: please ask your Teal connection to supply you with an API key +- A callback URL for receiving webhook notifications: generate your URL in [webhook.site](https://webhook.site/) + +## 1. Webhooks Setup `/webhooks` + +Configure webhooks to receive notifications for events such as new payroll submissions. + +```bash +curl --request POST \ + --url https://api.sandbox.goteal.co/webhooks \ + --header 'Content-Type: application/json' \ + --header 'X-API-KEY: ' \ + --data '{ + "events": [ + "user-payroll-submitted", + "user-payroll-created" + ], + "name": "user-payroll-submitted", + "secret": "very-secret", + "url": "https://webhooks.company.com" +}' +``` +**Response Example:** + +```json +{ + "webhook_id": "95a0e70b-fe02-4f47-aef9-2efff279df71", + "events": [ + "user-payroll-submitted", + "user-payroll-created" + ], + "name": "user-payroll-submitted", + "url": "https://webhooks.company.com", + "created_at": "2019-05-17T00:00:00.000Z" +} +``` + +## 2 Configure Client Settings `/configuration` + +Configure client-wide settings that affect sandbox behavior, such as payroll lookback periods and recurring checks. + +```bash +curl --request PUT \ + --url https://api.sandbox.goteal.co/configuration \ + --header 'Content-Type: application/json' \ + --header 'X-API-KEY: ' \ + --data '{ + "payslip_upload_enabled": true, + "payroll_period_months": 12, + "recurring_checks_enabled": true, + "recurring_check_frequency": "HOURLY", + "recurring_check_end_date": "2025-01-10T00:00Z" +}' +``` + +**Response Example:** + +```json +{ + "payslip_upload_enabled": true, + "payroll_period_months": 12, + "client_logo": null, + "client_callback_url": null, + "recurring_checks_enabled": true, + "recurring_check_frequency": "HOURLY", + "recurring_check_end_date": "2025-01-10T00:00:00.000Z" +} +``` + +In sandbox, you can test the `HOURLY` frequency (not available in production). + +## 3. Create a user `/users` + +Now you need to create a user selecting a user ID from the following users: + +```bash +curl --request POST \ + --url https://api.sandbox.goteal.co/users \ + --header 'Content-Type: application/json' \ + --header 'X-API-KEY: ' \ + --data '{ + "name": "John Smith", + "email": "john.smith@company.com" +}' +``` + +The field `email` does not have to be a real email address. It can be the user's identifier in your system. + +**Response Example:** + +```json +{ + "user_id": "95a0e70b-fe02-4f47-aef9-2efff279df71", + "name": "John Smith", + "email": "john.smith@company.com", + "created_at": "2019-05-17T00:00:00.000Z" +} +``` + +## 4. Generating a user token `/user-tokens` + +After creating a webhook, generate a token to authenticate subsequent actions for that user. The token ensures that actions such as account connection and payslip uploads are securely attributed to the correct user. + +```bash +curl --request POST \ + --url https://api.sandbox.goteal.co/user-tokens \ + --header 'Content-Type: application/json' \ + --header 'X-API-KEY: ' \ + --data '{ + "user_id": "95a0e70b-fe02-4f47-aef9-2efff279df71" +}' +``` + +**Response Example:** + +```json +{ + "created_at": "2019-05-17T00:00:00.000Z", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiOTVhMGU3MGItZmUwMi00ZjQ3LWFlZjktMmVmZmYyNzlkZjcxIiwiaWF0IjoxNTE2MjM5MDIyfQ.4S5J" +} +``` + +## 5. Connecting a Payroll Account `/accounts` + +This simulates the user connecting their payroll account for automatic data retrieval. + +```bash +curl --request POST \ + --url https://api.sandbox.goteal.co/accounts \ + --header 'Content-Type: application/json' \ + --header 'X-API-KEY: ' \ + --data '{ + "user_id": "95a0e70b-fe02-4f47-aef9-2efff279df71", + "password": "very-secret-password", + "payroll_provider": "quickbooks", + "user_name": "john.smith@company.com" +}' +``` + +**Response Example:** + +```json +{ + "account_id": "95a0e70b-fe02-4f47-aef9-2efff279df71", + "created_at": "2019-05-17T00:00:00.000Z", + "payroll_provider": "quickbooks", + "user_name": "john.smith@company.com" +} +``` + +As with production accounts, when connecting we will retrieve payroll immediately. For sandbox, the number of payroll submissions generated +matches the `payroll_period_months` configuration. If this is set on both the client and user configuration, the user level config takes +precedence. For example, if it is set to 7 months, we'll return 8 payroll submissions (including the current month). If it is not set we'll +take a default of 36 months. To simulate a gap in the payroll, the `Shape` provider will leave a 3 month gap from the time of the request. + +## 6. Retrieve submitted Payroll information `/entries/connections` + +Retrieve latest payroll for a connected account. For sandbox, we'll always return new payroll where the `pay_date` is set to the current day. For `Shape` provider we will still maintain a 3 month gap. + +```bash +curl --location --request POST 'https://api.sandbox.goteal.co/entries/connections' \ +--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfbmFtZSI6InN5c19jbGllbnQxIiwidXNlcl9pZCI6IjU3YWRiMzg0LWE2ZGEtNGE1Ny05NThhLTU1ZDk5MzdjZWE0MyIsImlzcyI6ImFwcC5zYW5kYm94LmdvdGVhbC5jbyIsImV4cCI6MTcxNTM0NTI5MH0.IxgmSN4XAn7j2jbQu0npwLXQjEQVqcbjmIBO2T-NCAc' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "account_id" : 552300dd-1e91-48d4-958b-41736c2c0167 +}' +``` + +**Response Example:** +```json +{ + "account_id": "552300dd-1e91-48d4-958b-41736c2c0167", + "entry_id": "bdc420fe-5e48-4802-a753-659979a2a346", + "payroll_submissions": [ + { + "id": "0187c66e-e7e5-811c-b006-2232f00f426a", + "account_id": "552300dd-1e91-48d4-958b-41736c2c0167", + "entry_id": "bdc420fe-5e48-4802-a753-659979a2a346", + "created_at": "2024-05-03T14:00:13.804Z", + "identity_information": { + "name": "Jane Doe", + "date_of_birth": "1990-04-01", + "address": { + "street": "123 Main St", + "county": "Anyshire", + "city": "Anycity", + "post_code": "AB12 3CD", + "country": "Countryland" + }, + "email": "jane.doe@example.com", + "phone": "+1234567890", + "NI_number": "AB123456C" + }, + "employment_information": { + "employer_name": "Acme Corp", + "role": "Software Developer", + "type": "Full-Time", + "status": "Active", + "start_date": "2018-06-01", + "leave_date": null + }, + "income_information": { + "pay_date": "2024-02-15", + "pay_interval_start": "2024-02-01", + "pay_interval_end": "2024-02-15", + "pay_frequency": "weekly", + "earnings": { + "gross_pay": 2000, + "net_pay": 1500, + "base_salary": 1900, + "bonus": 100 + }, + "deductions": { + "income_tax": 300, + "employee_ni": 150, + "employee_pension": 50, + "total_deductions": 500 + } + } + }, + { + "id": "0187c66e-e7e5-811c-b006-2232f00f426a", + "account_id": "552300dd-1e91-48d4-958b-41736c2c0167", + "entry_id": "bdc420fe-5e48-4802-a753-659979a2a346", + "created_at": "2024-05-03T14:00:13.804Z", + "identity_information": { + "name": "Jane Doe", + "date_of_birth": "1990-04-01", + "address": { + "street": "123 Main St", + "county": "Anyshire", + "city": "Anycity", + "post_code": "AB12 3CD", + "country": "Countryland" + }, + "email": "jane.doe@example.com", + "phone": "+1234567890", + "NI_number": "AB123456C" + }, + "employment_information": { + "employer_name": "Acme Corp", + "role": "Software Developer", + "type": "Full-Time", + "status": "Active", + "start_date": "2018-06-01", + "leave_date": null + }, + "income_information": { + "pay_date": "2024-02-15", + "pay_interval_start": "2024-02-01", + "pay_interval_end": "2024-02-15", + "pay_frequency": "weekly", + "earnings": { + "gross_pay": 2000, + "net_pay": 1500, + "base_salary": 1900, + "bonus": 100 + }, + "deductions": { + "income_tax": 300, + "employee_ni": 150, + "employee_pension": 50, + "total_deductions": 500 + } + } + }, + { + "id": "0187c66e-e7e5-811c-b006-2232f00f426a", + "account_id": "552300dd-1e91-48d4-958b-41736c2c0167", + "entry_id": "bdc420fe-5e48-4802-a753-659979a2a346", + "created_at": "2024-05-03T14:00:13.804Z", + "identity_information": { + "name": "Jane Doe", + "date_of_birth": "1990-04-01", + "address": { + "street": "123 Main St", + "county": "Anyshire", + "city": "Anycity", + "post_code": "AB12 3CD", + "country": "Countryland" + }, + "email": "jane.doe@example.com", + "phone": "+1234567890", + "NI_number": "AB123456C" + }, + "employment_information": { + "employer_name": "Acme Corp", + "role": "Software Developer", + "type": "Full-Time", + "status": "Active", + "start_date": "2018-06-01", + "leave_date": null + }, + "income_information": { + "pay_date": "2024-02-15", + "pay_interval_start": "2024-02-01", + "pay_interval_end": "2024-02-15", + "pay_frequency": "weekly", + "earnings": { + "gross_pay": 2000, + "net_pay": 1500, + "base_salary": 1900, + "bonus": 100 + }, + "deductions": { + "income_tax": 300, + "employee_ni": 150, + "employee_pension": 50, + "total_deductions": 500 + } + } + } + ] +} +``` + +## 7. Extract Payroll information from a payslip `/entries/payslips` + +This request simulates the user uploading a payslip to extract payroll information. +Extracting payslip information can be done using the endpoint below but will usually +be done by the user through the WebApp as per section 8 + +```bash +curl --location --request POST 'https://api.sandbox.goteal.co/entries/payslips' \ +--header 'Authorization: Bearer ' \ +--form 'file=@""' +``` + +**Response Example:** + +```json +{ + "account_id": "95a0e70b-fe02-4f47-aef9-2efff279df71", + "entry_id": "f5e0a310-d4d0-4db0-b5c3-895a81e26e03", + "payroll_submissions": [ + { + "id": "0187c66e-e7e5-811c-b006-2232f00f426a", + "account_id": null, + "entry_id": "f5e0a310-d4d0-4db0-b5c3-895a81e26e03", + "created_at": "2024-05-03T12:48:54.886Z", + "identity_information": { + "name": "Jane Doe", + "date_of_birth": "1990-04-01", + "address": { + "street": "123 Main St", + "county": "Anyshire", + "city": "Anycity", + "post_code": "AB12 3CD", + "country": "Countryland" + }, + "email": "jane.doe@example.com", + "phone": "+1234567890", + "NI_number": "AB123456C", + "employment_information": { + "employer_name": "Acme Corp", + "role": "Software Developer", + "type": "Full-Time", + "status": "Active", + "start_date": "2018-06-01", + "leave_date": null + }, + "income_information": { + "pay_date": "2024-02-15", + "pay_interval_start": "2024-02-01", + "pay_interval_end": "2024-02-15", + "pay_frequency": "weekly", + "earnings": { + "gross_pay": 2000, + "net_pay": 1500, + "base_salary": 1900, + "bonus": 100 + }, + "deductions": { + "income_tax": 300, + "employee_ni": 150, + "employee_pension": 50, + "total_deductions": 500 + } + } + } + } + ] +} +``` + +## 8. Retrieving Payroll Data `/payroll` + +Retrieve the payroll data submitted through the connected account or document upload. + +```bash +curl --request GET \ + --url https://api.sandbox.goteal.co/payroll/{user_id} \ + --header 'X-API-KEY: ' +``` +**Response Example:** + +```json +{ + "pagination": { + "count": 14, + "limit": 25, + "offset": 75, + "total_count": 89 + }, + "payroll_submissions": [ + { + "id": "95a0e70b-fe02-4f47-aef9-2efff279df71", + "account_id": "0988b4bd-42dd-4556-95ae-27a906c066ec", + "entry_id": "0d2bcb9b-a7ec-463f-bd7e-aacaa1a190a4", + "created_at": "2019-05-17T00:00:00.000Z", + "document_external_id": null, + "document_filename": null, + "employment_information": { + "employer_name": "Acme Ltd", + "leave_date": "2019-05-17T00:00:00.000Z", + "role": "Software Engineer", + "start_date": "2019-05-17T00:00:00.000Z", + "status": "Active", + "type": "Full-time" + }, + "identity_information": { + "NI_number": "AB123456C", + "address": { + "city": "London", + "country": "United Kingdom", + "county": "Greater London", + "post_code": "SW1A 1AA", + "street": "123 Main Street" + }, + "date_of_birth": "2019-05-17T00:00:00.000Z", + "email": "john.smith@company.com", + "name": "John Smith", + "phone": 447123456789 + }, + "income_information": { + "deductions": { + "employee_ni": 200, + "employee_pension": 300, + "income_tax": 500, + "total_deductions": 1000 + }, + "earnings": { + "base_salary": 3000, + "bonus": 500, + "gross_pay": 3500, + "net_pay": 2500 + }, + "pay_date": "2019-05-17T00:00:00.000Z", + "pay_frequency": "Monthly", + "pay_interval_end": "2019-05-17T00:00:00.000Z", + "pay_interval_start": "2019-05-17T00:00:00.000Z" + } + } + ] +} +``` + +## 9. WebApp usage + +Teal's webapp can be tested by accessing ```https://app.sandbox.goteal.co/``` in the browser. Use the generated bearer +token in order to identify the user ```https://app.sandbox.goteal.co/?jwt=```. The webapp uses the same API +mechanism as the mentioned above, therefore the data will be retrieved in the same way. + +**Connect a Payroll:** + +payroll connection + +**Submit a Payroll:** + +payslip submission + +## 10. Cleanup: Deleting a User `/users` + +After testing, clean up by deleting the test user and their associated data. + +```bash +curl --request DELETE \ + --url https://api.sandbox.goteal.co/users/{user_id} \ + --header 'X-API-KEY: ' +``` + +**Response Example:** + +```json +{ + "message": "User deleted successfully." +} +``` + +## Sandbox credentials + +Some of our providers use an authorisation flow know as OAuth2. This allows the the end user to be redirected to their +payroll provider login screen, where they can grant consent to share their data. Below is an example login page: + +OAuth2 Login + +The user will be redirected back to teal after authorising. + +To test the providers that have the OAuth2 flow available, you can use the following credentials. + +| provider | username | password | +| ----------- | --------------- | ---------------- | +| `freeagent` | arun@goteal.co | mPxD9Tt3hBV5j39 | \ No newline at end of file