Authorize

#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

  1. Author a manifest: a named set of members. A member is either one resource you own, or an include of another of your manifests.
  2. Offer it to one tenant. The offer is a publication.
  3. The other tenant accepts it. Until then they can review the offer, but nothing is shared.
  4. 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 entity member), 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.

  • name is required, at most 200 characters, and unique in your tenant. Names are case-sensitive. A second manifest with the same name is refused with 409 (MANIFEST_NAME_TAKEN).
  • description (up to 2,000 characters) and metadata (any JSON object) are optional. Neither affects what is shared.
  • members is 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. For entity, 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 for job_definition given by name. When a job name has several versions, a bare name is refused with 409 (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_definition or app_spec member.
  • Every entity type it uses: the types its triggers watch, and the types its steps, widgets and queries read or write. Use entity_type members. A type's own attribute schema comes with the type. Don't list it separately.
  • Every JSON schema its configSchemas names. Each one is a json_schema member. If one is missing, the other tenant's config writes and workflow runs fail with UNKNOWN_CONFIG_SCHEMA. See Config Schemas.
  • Your rows the other tenant should read: an entity member per type (or per row).
  • Relationship rules the other tenant needs to see, as relationship_rule members.
  • 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_definition member. 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
  • members replaces the whole set, so send every member you want to keep. Leave members out to change only name, description or metadata. Members get new ids on every replace.
  • metadata is replaced as a whole document. null clears it, and so does null on description.
  • 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