Build your own connector
How to connect SourceLace to a system it has no connector for, such as an in-house database, an internal API or a niche app, by building a small web service of your own. This page is the complete protocol: if something you need is not written here, it is not part of the protocol, so tell support@sourcelace.com and we will add it here.
Status: new. The protocol is version 1 and covered by automated tests; it has not yet been used with a customer's live connector. Admins add one as Custom connector on Manage sources.
How it works
A connector is a small HTTPS web service that you write and run, in any language. SourceLace calls it when someone asks about your system: "what objects are there", "what fields does this one have", "run this query", "get this record", and, if you allow it, "check this change" and "make this change". Every call is signed with a secret you share with SourceLace, and names the person who is asking, so your connector applies your own permissions.
Your code never runs inside SourceLace, and SourceLace never sends your connector anyone's passwords or tokens: only the signed request and the person's email, name (if known), organization id and SourceLace group names.
SourceLace does:
- signs people in, and checks that the person may use this source at all (access by group);
- previews every change and applies it only after the person confirms it, and only when an admin has turned on changes for this source and object;
- writes every call to the audit trail;
- holds results in memory briefly (never in a database), caps the rows it keeps, and stops waiting for your connector after 30 seconds.
Your connector must:
- check the signature and timestamp of every request (see Signing requests);
- enforce your own system's permissions: use
personto decide what this person may read and change, and return or change nothing they may not; - keep
queryread-only: whatever the query text says,/querymust not change any data. Run it with a read-only login or connection, or accept only a format you can check. SourceLace only checks that a query is not empty and at most 10,000 characters, because only your connector knows its query language; - never put passwords, keys or internal addresses in an answer:
error.messageis shown to the person.
Add a custom source (SourceLace admin)
- Make a shared secret, a random string of 32 to 256 characters, for example with
openssl rand -hex 32. Keep it in your password manager and give it to whoever runs the connector. - On Manage sources → Add a source, choose Custom connector (
custom), give it a name (such ascustom:inventory) and a label, and fill in the options below. - To allow changes, turn on Allow changes and list the objects that may change. The connector must also say
"writes": truein its manifest. - People connect the source once (there is no extra sign-in) and then ask about it like any other source.
| Option | Type | Default | Example | What it is |
|---|---|---|---|---|
url |
Web address | (required) | https://connector.corvanta.com |
The connector's address. Must start with https://, with no user name, password, ?query or #fragment. A path is fine, such as https://api.corvanta.com/sourcelace-connector. |
secret |
Secret, 32 to 256 characters | (required) | The shared secret. Stored encrypted with your organization's key and never shown again. |
In SourceLace's cloud, the connector's address must be on the public internet: private addresses are refused.
Transport
| Rule | Value |
|---|---|
| Base URL | The url option. Every path below is appended to it. |
| Scheme | https:// only. |
| Methods | GET for the manifest; POST for everything else. |
| Request body | UTF-8 JSON, Content-Type: application/json. SourceLace sends compact JSON, but don't depend on spacing or key order: parse it. |
| Success | HTTP 200 with a UTF-8 JSON body. Any other status is an error (see Errors). |
| Redirects | Not followed. A 3xx answer is an error. |
| Time limit | SourceLace waits at most 30 seconds for the whole answer. |
| Size limit | An answer must be at most 10 MB (10,000,000 bytes). |
| Unknown fields | Both sides ignore JSON fields they don't know. Later versions may add optional fields; they will never change or remove the ones here. |
| Null | A field marked "or null" may be null or left out; both mean "no value". |
Signing requests
Every request, including the GET, has two headers:
| Header | Value |
|---|---|
SourceLace-Timestamp |
When SourceLace sent the request: whole seconds since 1970-01-01T00:00:00Z, as digits, such as 1767225600. |
SourceLace-Signature |
v1= followed by 64 lowercase hexadecimal characters. |
SourceLace computes the signature like this, and your connector must check it the same way:
- Take the raw request body exactly as the bytes arrived. Don't parse and re-encode it first. For the
GETrequest the body is empty: zero bytes. - Build the message: the timestamp header's value as ASCII bytes, then one
.(byte0x2E), then the body bytes. - Compute HMAC-SHA256 of the message, keyed with the secret's UTF-8 bytes.
- Write the 32-byte result as 64 lowercase hexadecimal characters and put
v1=in front.
Refuse the request (HTTP 401, error code unauthorized) when either header is missing, the timestamp is not only digits, the timestamp is more than 300 seconds before or after your own clock (keep your clock synchronized with NTP), or the signature is not exactly equal to the one you compute. Compare with a constant-time comparison, such as hmac.compare_digest in Python, crypto.timingSafeEqual in Node.js or MessageDigest.isEqual in Java.
To change the secret without downtime, a connector may accept two secrets for a while: add the new one next to the old one in your connector, change it in SourceLace, then remove the old one.
Worked example
Your code must produce exactly these signatures.
| Secret | sourcelace-example-secret-0123456789 |
| Timestamp | 1767225600 |
| Body (110 bytes, no line break at the end) | {"person":{"email":"ann@corvanta.com","name":"Ann Lee","tenant":"corvanta","groups":["Sales"]},"text":"stock"} |
| Message that is signed | 1767225600. followed by the body |
SourceLace-Signature |
v1=fefe61f4029cc019764e6d73619e17f4798c43292f94e370244fd17116c27623 |
For a GET (empty body) with the same secret and timestamp, the message is 1767225600. and the signature is v1=5c4452cf2f35fa29be49a2e8d9efc44409c6dd800b9abf2c17bf0a27a56f8548.
Check it from a shell:
printf '%s' '1767225600.{"person":{"email":"ann@corvanta.com","name":"Ann Lee","tenant":"corvanta","groups":["Sales"]},"text":"stock"}' \
| openssl dgst -sha256 -hmac 'sourcelace-example-secret-0123456789'
# ... fefe61f4029cc019764e6d73619e17f4798c43292f94e370244fd17116c27623
Endpoints
Every POST body has a person object. SourceLace verified who this is; once the signature checks, you can trust it.
person field |
Type | Meaning |
|---|---|---|
email |
string, 1-320 characters | The person's email address, in lowercase. |
name |
string (up to 200) or null | Their display name, or null when SourceLace does not know it. |
tenant |
string, 1-100 characters | Their organization's id in SourceLace, such as corvanta. One connector can serve several organizations; use this to tell them apart. |
groups |
array of strings (up to 500) | The person's SourceLace group names (set by an admin, or synced from single sign-on). May be empty. |
The lengths below are maximums your answers must respect. SourceLace treats an answer that breaks any rule here as a failed call, and does not use it.
GET /sourcelace/v1/manifest
What the connector is. No body. SourceLace asks for it when someone connects the source, and keeps the answer for 5 minutes.
| Field | Type | Required | Meaning |
|---|---|---|---|
protocol |
integer, 1 |
no (default 1) |
The protocol version. Only 1 exists. |
name |
string, 1-100 | yes | The system's name, such as Corvanta inventory. |
description |
string, 1-1000 | yes | What is in it, in a sentence or two. |
version |
string, 1-50 | yes | Your connector's own version, such as 1.0.0. |
query_language.name |
string matching ^[a-z][a-z0-9_]{0,39}$ |
yes | A short name, such as inventory_sql. |
query_language.description |
string, 1-2000 | yes | One paragraph that tells the AI how to write a query: the objects or tables, the syntax, and what is refused. Write it for a reader who has never seen your system. |
query_language.examples |
1 to 3 strings, each 1-1000 | yes | Complete example queries. |
writes |
boolean | no (default false) |
true when the two changes endpoints work. |
objects_searchable |
boolean | no (default false) |
true when /objects filters by text itself. Otherwise SourceLace keeps the objects whose name, label or description contains the text. |
{
"protocol": 1,
"name": "Corvanta inventory",
"description": "Products, stock levels and warehouses from Corvanta's inventory system.",
"version": "1.0.0",
"query_language": {
"name": "inventory_sql",
"description": "One SQLite SELECT statement on the tables products and warehouses. Anything that changes data is refused.",
"examples": ["SELECT sku, name, in_stock FROM products WHERE in_stock < 100 ORDER BY in_stock"]
},
"writes": true,
"objects_searchable": false
}
POST /sourcelace/v1/objects
The objects (tables, entities, record types) this person can read.
- Request:
person;text, 0-200 characters, what the person is looking for (empty means all). - Answer:
objects, at most 1,000, each withname(1-200, required, used in every other call),label(up to 200, or null) anddescription(up to 1,000, or null).
{"objects": [{"name": "products", "label": "Products", "description": "Everything we sell."}]}
POST /sourcelace/v1/describe
One object's fields.
- Request:
person;object, a name from/objects. - Answer:
name, optionallabelanddescription, andfields: 1 to 1,000 fields with unique names, each withname(1-200),type, optionallabelanddescription,key(truefor the one field whose value/recordtakes asid; at most one) andwritable(truewhen a change may set it). If there is no such object, answernot_found.
| Type | JSON value in rows |
|---|---|
string |
string |
integer |
number without a fraction |
number |
number |
boolean |
true or false |
date |
string, YYYY-MM-DD |
datetime |
string, ISO 8601 with a time zone, such as 2026-10-03T14:05:00Z |
json |
any JSON value |
Any value may be null.
POST /sourcelace/v1/query
Run one read-only query in your query language.
- Request:
person;query, 1-10,000 characters;max_rows, 1-10,000. SourceLace asks for one more row than it keeps, to tell whether a result was cut short. - Answer:
columns(at most 500 unique names, in display order),rows(at mostmax_rowsobjects whose keys are names fromcolumns; a missing key means null) andtruncated(truewhen more rows matched than you returned). - If the query is not valid, or would change anything, answer
invalid_querywith a message that says how to fix it: SourceLace passes it to the AI, which uses it to try again.
{"columns": ["sku", "in_stock"], "rows": [{"sku": "SKU-2", "in_stock": 0}], "truncated": false}
POST /sourcelace/v1/record
One record by the value of its key field.
- Request:
person;object;id, 1-500 characters (the key field's value, as text). - Answer:
row, an object of field name to value. If there is no such object or record, or the person may not see it, answernot_found.
{"row": {"sku": "SKU-1", "name": "Trail running shoe", "price": 129.0}}
Changes (optional): /changes/check and /changes/apply
POST /sourcelace/v1/changes/check and POST /sourcelace/v1/changes/apply, only when the manifest says "writes": true. SourceLace calls check to build the preview the person sees, and apply only after they confirm it.
A change, in both requests next to person, has operation (create, update or delete), object, record_id (null for create) and fields (at most 100 new values; {} for delete, at least one otherwise).
/changes/check must not write anything. It answers allowed (boolean, required), problems (up to 50 reasons, for the person), before (the record's current values of the fields being changed), record_name (shown in the preview) and version (the record's current version, such as a last-modified time; SourceLace sends it back to apply). If the record does not exist, answer not_found; if the person may not change it, forbidden, or allowed: false with problems.
/changes/apply makes the change. Its request also has version, as check returned it (or null). If the record's version is no longer version, don't write and answer conflict. It answers record_id, the record's id (the new id after a create).
{"record_id": "SKU-4"}
Errors
Any answer other than HTTP 200 is an error. Its body should be:
{"error": {"code": "not_found", "message": "There is no product SKU-9."}}
message is 1-1,000 characters and is shown to the person and their AI, so write it for them and never include secrets.
code |
HTTP status | When |
|---|---|---|
invalid_request |
400 | The request does not follow this protocol (you will mostly see this while testing). |
invalid_query |
400 | The query is not valid in your language, or would change data. |
unauthorized |
401 | The signature or timestamp check failed. |
forbidden |
403 | This person may not do this. |
not_found |
404 | No such object or record (or the person may not know it exists). |
conflict |
409 | /changes/apply only: the record changed since /changes/check. |
unavailable |
503 (or 500 for an unexpected failure) | Your system cannot answer right now. |
An error answer without this body, a timeout, a refused connection or an invalid answer is reported to the person as a failure of the connector, without details.
Limits
| Limit | Value |
|---|---|
| Time per answer | 30 seconds |
| Size per answer | 10 MB |
| Query text | 10,000 characters |
| Rows per query request | 10,000 (and SourceLace keeps at most 2,000) |
Objects per /objects answer |
1,000 |
| Fields per object | 1,000 |
| Columns per query | 500 |
| Fields per change | 100 |
| Signature clock tolerance | 300 seconds |
| Shared secret | 32 to 256 characters |
Versioning
This is version 1, and the path contains it (/sourcelace/v1/). Additions that are optional on both sides stay in version 1, so ignore what you don't know. Anything that would break an existing connector will be version 2, under /sourcelace/v2/, and SourceLace will keep calling version 1 connectors.
Building one
Any language that can serve HTTPS and compute HMAC-SHA256 works:
- Implement signature checking first and test it against the worked example.
- Implement the manifest and the four
POSTendpoints (objects,describe,query,record), and the twochangesendpoints only if you want changes. - Answer errors in the shape above.
- Run it behind HTTPS on the internet (any container platform, a virtual machine behind a reverse proxy, or your own Kubernetes), with the secret as a secret setting.
SourceLace also has a Python starter kit, sourcelace-connector (Python 3.11 or newer): you write one class with objects, describe, query and record methods plus a manifest, and the kit handles signing, validation and errors. Its sourcelace-connector check <address> --secret <secret> command calls every endpoint the way SourceLace does and prints a pass or fail list, for a connector in any language, without changing anything. It is not yet published; ask support@sourcelace.com for a copy.
When something goes wrong
| What you see | What to do |
|---|---|
| "Set the option 'url': the connector's address, such as https://connector.corvanta.com." | Fill in url. |
| "The connector's url must start with https://..." | Use the connector's https:// address. |
| "The option 'url' must be a plain address such as https://connector.corvanta.com, with no user name, password, ?query or #fragment." | Remove those parts from url. |
| "Set the option 'secret': the secret shared with the connector, 32 to 256 characters..." | Enter the shared secret. |
| "The address of ...'s connector (...) could not be found." | Check the host name in url. |
| "... is a private network address, and this SourceLace server only calls connectors on the internet..." | Give the connector a public https:// address. |
| "...'s connector refused SourceLace's signature (...). An admin should check that the secret on SourceLace's Sources page matches the connector's, and that the connector's clock is right." | Make the secrets match, and sync the connector's clock. |
| "...'s connector did not answer within 30 seconds." | Make the connector faster, or ask for less. |
| "...'s connector sent an answer larger than 10 MB." | Return fewer rows or smaller values. |
| "...'s connector sent an answer that does not follow the SourceLace connector protocol, so SourceLace did not use it..." | Whoever maintains the connector runs sourcelace-connector check to see what is wrong. |
| "...'s connector failed (HTTP ...). Try again; if it keeps failing, tell your admin." | The connector answered with an error but no error body. Check its logs. |
| "... refused the query: ..." | The connector's own reason follows; rewrite the query. |
| "... does not accept changes: its connector is read-only." | The manifest says "writes": false. |