#Share with Another Tenant
You built a workflow, an app spec, or a data model, and another tenant wants to use it. This guide is the owner side: you put what they need into a manifest, offer it to their tenant, and manage it from then on. The other tenant's side (reviewing, accepting, activating) is in Accept & Activate. What a share lets a tenant do is in Shares & Grants.
Authenticate with a Personal Access Token (HTTP Basic auth). Enter it once in the Authorize bar above and every Try it below uses it.
#How sharing works
- Author a manifest: a named set of members. A member is either one
resource you own, or an
includeof another of your manifests. - Offer it to one tenant. The offer is a publication.
- The other tenant accepts it. Until then they can review the offer, but nothing is shared.
- On acceptance, every resource the manifest resolves to becomes a read-only share to that tenant. If you change the members later, the change applies to every tenant that accepted, straight away.
Nothing else creates a share. A workflow definition, job definition or app spec has no sharing section, and nothing is inferred from what a definition references. Every resource the other tenant needs must be a member.
#Who can call what
| Call | Who |
|---|---|
Read: list and fetch manifests, /resolved, the publication roster, GET /access/shares |
Any caller in your tenant |
| Write: create, update, delete a manifest; offer; withdraw an offer | A tenant admin, using a token that is not bound to a policy |
A write by any other caller is refused with 403 FORBIDDEN ("Tenant admin role
required to perform this action."). This is also the answer for a tenant admin's
policy-bound token. The publication roster is readable by the owner tenant only.
Any other tenant gets 404.
#What the other tenant gets
- ✅ They can read each shared resource, and activate a shared workflow or app spec in their own tenant, under their own configuration and policies.
- ✅ They can store their own rows of a shared entity type.
- ❌ They can't write to anything of yours. A share is read-only.
- ❌ They can't re-share your resources to anyone else.
- ❌ Nothing is copied. When you withdraw the offer or remove a member, their access ends on their next request.
- ❌ Seeing your rows is not automatic, even for a shared type. You must
list the rows (an
entitymember), and the other tenant decides which of its users see them. See The owner's data in your tenant.
#Author a manifest
POST/access/manifestsCreate a manifestAPI docs ↗Try it
The response is 201 with the manifest, its members (each with its own id,
and the resolved resource_id and resource_name), a revision, and a
publication_summary counting your offers by status.
nameis required, at most 200 characters, and unique in your tenant. Names are case-sensitive. A second manifest with the same name is refused with409(MANIFEST_NAME_TAKEN).description(up to 2,000 characters) andmetadata(any JSON object) are optional. Neither affects what is shared.membersis a set, so list each resource once. It comes back in a stable order that is not the order you sent.
#Member types
resource_type |
What the other tenant can do after they accept |
|---|---|
workflow_definition |
Read the workflow and activate it in their tenant |
app_spec |
Read the app spec by id and activate it in their tenant |
job_definition |
Read that one job version, and bind it in their own definitions and activations |
entity_type |
See the type and its attribute schema, and store their own rows of it. Your rows stay hidden |
entity |
By type (a type name, or the type's id): all your rows of that type. By a row's id: that row only. The row's type is always shared with it. The other tenant decides which of its users read them |
json_schema |
Read a standalone JSON schema, for example one named in a definition's configSchemas |
relationship_rule |
See the rule, and link their own rows to your rows with it. Share the type their side uses (entity_type) and the rows they link to (entity) too; see Links to shared rows |
#Name or id
Give each resource member exactly one of these:
resource_name: the resource's name, looked up in your tenant when you save. The match ignores case. Names travel between environments, so this is the portable form. Forentity, a name is the type name and means all rows of that type.resource_id: the resource's id. Use it to name one entity row, or any resource that must be pinned exactly. The resource must exist and be owned by your tenant.resource_version: only forjob_definitiongiven by name. When a job name has several versions, a bare name is refused with409(MANIFEST_NAME_AMBIGUOUS). Name the version you mean:
{
"resource_type": "job_definition",
"resource_name": "notify-expiry",
"resource_version": "1.0.0"
}
permitted_actions defaults to ["read"], which is the only action a share can
carry. Set it to [] to share a resource as listing-only, with no read
action. An entity type shared this way is still visible to the other tenant, and
they can store their own rows of it.
Names are looked up before anything is written. If one member doesn't resolve or
isn't yours, the whole request is refused with 422 (MANIFEST_MEMBER_INVALID,
and details.member names it). Nothing is saved.
#What to list for a workflow or an app
A definition's references are not followed for you. Before you offer a workflow or an app spec, list:
- The definition itself: a
workflow_definitionorapp_specmember. - Every entity type it uses: the types its triggers watch, and the types
its steps, widgets and queries read or write. Use
entity_typemembers. A type's own attribute schema comes with the type. Don't list it separately. - Every JSON schema its
configSchemasnames. Each one is ajson_schemamember. If one is missing, the other tenant's config writes and workflow runs fail withUNKNOWN_CONFIG_SCHEMA. See Config Schemas. - Your rows the other tenant should read: an
entitymember per type (or per row). - Relationship rules the other tenant needs to see, as
relationship_rulemembers. - Jobs, following the rules below.
A workflow whose trigger watches Task and whose configSchemas.env names
task_env needs three members:
{
"members": [
{ "resource_type": "workflow_definition", "resource_name": "process-task" },
{ "resource_type": "entity_type", "resource_name": "Task" },
{ "resource_type": "json_schema", "resource_name": "task_env" }
]
}
#Jobs
- Jobs you own and bound in the definition's default job bindings go with the definition. The other tenant reads and runs them without a member.
- A job you borrowed (one another tenant shared with you) does not go with your definition. Its owner has to put it in a manifest offered to your consumer, and your consumer has to accept that manifest. See Job Bindings.
- A job the other tenant should bind themselves (outside your default
bindings) must be a
job_definitionmember. Only the version you name is shared.
Sharing a definition shows what it binds. Any tenant that can run your workflow or app can read the bodies of the jobs it binds. Check its default bindings before you offer it. See What a share discloses.
#Compose with includes
A member { "include": "<manifest id or name>" } pulls in another of your
manifests. Use it for a shared library, such as a catalog of entity types that
several app manifests need:
{ "include": "shipping-catalog" }
- One acceptance of the top manifest covers everything it includes. You don't
offer the included manifests separately, and the other tenant can't read them.
They see what the includes contribute through
/resolved. - When you change an included manifest's members, every manifest that includes it changes too, and so does every tenant that accepted one of them.
- Includes stay inside your tenant. An include is matched by id, or by its exact, case-sensitive name. An include chain can be up to eight manifests deep, counting the top one. A cycle, including a manifest that includes itself, is refused.
- If you delete a manifest, every manifest that included it loses that include.
The other option is sibling manifests: separate manifests offered to the same tenant, each with its own offer and acceptance. When two manifests the tenant accepted list the same resource, they get one share of it with two sources. Withdrawing one manifest keeps the share while the other still lists it.
#Preview the effect
/resolved flattens a manifest into every resource it shares. This includes
what its includes bring in, and the entity types that go with entity members:
GET/access/manifests/{manifestId}/resolvedPreview every resource the manifest sharesAPI docs ↗Try it
Each element has resource_type, resource_id, resource_name,
resource_version, permitted_actions, the member_id behind it, and a
provenance that says why it is there:
provenance |
Meaning |
|---|---|
member |
A resource member of this manifest |
include:<manifest id> |
A resource member of an included manifest |
pairing:<member id> |
The entity type that goes with an entity member |
revision identifies the version you previewed. It increases whenever the
members or the metadata change, including a member change in an included
manifest. Renaming or editing the description leaves it unchanged. The other
tenant can call /resolved too, to review your offer before they accept.
#Offer it
You need the other tenant's id. Ask them for it: any row of their
GET /access/users carries it as tenant_id.
POST/access/manifests/{manifestId}/publicationsOffer the manifest to a tenantAPI docs ↗Try it
A new offer answers 201 with the publication in status offered. There is
only ever one publication per manifest and tenant, so offering again is safe:
| Current status | Offering again |
|---|---|
offered |
200, unchanged |
accepted |
200, unchanged. An acceptance is never reset |
declined |
200, offered again. The tenant must accept again |
revoked |
200, offered again with the same publication id. The tenant must accept again |
You can't offer a manifest to your own tenant (PUBLISH_SELF_TENANT), and the
tenant id must exist (PUBLICATION_GRANTEE_UNKNOWN). Both are 422.
See who you've offered it to, and where each offer stands:
GET/access/manifests/{manifestId}/publicationsList the manifest's offersAPI docs ↗Try it
Each publication has status (offered, accepted, declined or revoked)
and the offered_at, accepted_at and revoked_at timestamps. In
GET /access/manifests, each of your manifests has a publication_summary
counting its offers by status.
#Change what you share
PATCH/access/manifests/{manifestId}Replace the manifest's membersAPI docs ↗Try it
membersreplaces the whole set, so send every member you want to keep. Leavemembersout to change onlyname,descriptionormetadata. Members get new ids on every replace.metadatais replaced as a whole document.nullclears it, and so doesnullondescription.- The update is all or nothing. If one member is refused, the manifest keeps its name, members and everything else.
- Every tenant that accepted sees the change right away: added members are shared and removed ones are unshared. They don't need to accept again.
#Withdraw
Withdraw one tenant's offer:
DELETE/access/manifests/{manifestId}/publications/{publicationId}Withdraw an offerAPI docs ↗Try it
The publication stays, with status revoked, so the other tenant can still see
why their access ended. Every share it justified is removed. Withdrawing twice
returns the same publication. To offer it again, see Offer it.
To stop sharing the manifest with everyone, delete it:
DELETE/access/manifests/{manifestId}Delete a manifestAPI docs ↗Try it
This deletes its members and publications, and every share that only they
justified. The response is { "data": { "success": true } }. A resource another
accepted manifest still lists stays shared.
#Audit what you share
GET/access/sharesList your shares (direction=outbound)API docs ↗Try it
Pass direction=outbound for what you share. inbound gives what is shared
with you, and all (the default) gives both. Results are paginated with page
and limit (up to 200). Each share is one resource shared with one tenant:
resource_type, resource_id, grantor_tenant_id, grantee_tenant_id,
permitted_actions, and sources[]. Each source names the publication_id,
the manifest_id and manifest_name, the provenance (as in
Preview the effect), and the member_id behind it.
A share with two sources is listed by two manifests, or twice within one. For
example, a type can be shared both as a member and as the type that goes with an
entity member. The share stays until its last source goes.
Shares can't be edited. To change one, change a manifest.
#Errors
Every error uses the standard envelope (see
Auth & API Gateway). For the sharing-specific cases,
error.code is the general class and error.details.code is the specific
reason:
| HTTP | error.code |
details.code |
When |
|---|---|---|---|
| 403 | FORBIDDEN |
— | A write by a caller who is not a tenant admin, or by a policy-bound token |
| 404 | NOT_FOUND |
— | The manifest doesn't exist or isn't visible to you. Owner-only routes answer 404 to everyone else |
| 409 | CONFLICT |
MANIFEST_NAME_TAKEN |
Another of your manifests has this name |
| 409 | CONFLICT |
MANIFEST_NAME_AMBIGUOUS |
A job_definition name matches several versions. Add resource_version |
| 409 | CONFLICT |
PUBLICATION_EXISTS |
Two offers to the same tenant raced. Retry. The repeat answers 200 |
| 422 | VALIDATION_FAILED |
— | The body doesn't match the schema: an unknown key, both or neither of resource_id/resource_name, an action other than read, or resource_version where it doesn't apply |
| 422 | UNPROCESSABLE_ENTITY |
MANIFEST_MEMBER_INVALID |
A member doesn't resolve or isn't owned by your tenant, or an include names no manifest of yours |
| 422 | UNPROCESSABLE_ENTITY |
MANIFEST_INCLUDE_FOREIGN |
An include names another tenant's manifest |
| 422 | UNPROCESSABLE_ENTITY |
MANIFEST_INCLUDE_CYCLE |
An include would create a cycle |
| 422 | UNPROCESSABLE_ENTITY |
MANIFEST_INCLUDE_DEPTH |
An include chain would be more than eight manifests deep |
| 422 | UNPROCESSABLE_ENTITY |
PUBLISH_SELF_TENANT |
You offered the manifest to your own tenant |
| 422 | UNPROCESSABLE_ENTITY |
PUBLICATION_GRANTEE_UNKNOWN |
grantee_tenant_id names no tenant |
| 503 | SERVICE_UNAVAILABLE |
MANIFEST_RESOLVE_RACE |
/resolved kept changing while it was being read. Retry |
#See also
- Accept & Activate: the other tenant's side.
- Shares & Grants: what a share conveys and how it combines with grants.
- Config Schemas · Job Bindings: the two things a shared definition most often needs.
- Workflows · Apps: authoring the definitions themselves.