Skip to content

Enrollment API

Confidential. Do not share without explicit permission.

Introduction

The RSL Platform Enrollment API enables RSL Collective Partners, including website builders, advertising networks, CDNs, and Collective Rights Organizations, to enroll their publisher customers in the RSL Collective so they can receive licensing royalties for AI use of their licensed content under the collective terms negotiated by the RSL Collective.

Design principles

  • Scalability. Supports repertoires containing up to 100 million content scope declarations.

  • Bulk synchronization. Uses complete repertoire declarations and batch report files instead of per-object mutations, reducing integration and operational complexity at scale.

  • Isolated workflows. Separates repertoire management and payment reporting from payment disbursement, so financial transfers can operate in a distinct, fully audited workflow.

  • Auditability. Captures repertoire snapshots, report versions, and request identifiers in a form suitable for reconciliation, support, and dispute resolution.

  • Controlled lifecycle management. Separates validation from activation and uses explicit schema versioning and sandbox-production isolation to support safe testing and rollout.

  • Operations and resilience. Supports explicit heartbeats, asynchronous job states, resumable file downloads, and idempotent requests.

Bulk file management

See the Files API for the shared objects used by the Enrollment API for uploading and downloading files:

  • Upload object: manages the upload and validation workflow used to create a file object
  • File object: represents a managed file and its metadata
  • Result object: describes the outcome of uploading and validating a file

Base URL

  • Production: https://api.rslcollective.org/enrollment/v1
  • Sandbox: https://sandbox.api.rslcollective.org/enrollment/v1

Operational heartbeats

The Enrollment API defines heartbeat requirements to help Partners confirm that their production integration with the Enrollment API is still operational. Active Partners must successfully call at least one Enrollment API endpoint every 24 hours using valid production credentials, and any successful Enrollment API request with valid credentials satisfies the heartbeat requirement.

If the heartbeat expires, the RSL Platform sends an operational alert to the operations email address configured in the partner developer dashboard. The expired condition remains in effect until a subsequent Enrollment API request completes successfully.

Operational limits

The following operational limits apply to the Enrollment API:

LimitValue
Maximum rows per repertoire file100 million rows
Maximum repertoire file size5 GB
Minimum retention for report files12 months
Minimum retention for indexnow files12 months
Minimum retention for result files24 hours
Minimum temporary upload URL validity24 hours
Minimum temporary download URL validity24 hours

If a request exceeds an applicable operational limit, the API returns the error limit_exceeded.

The Licensee object

The Licensee object represents an entity that has entered into a collective licensing agreement with the RSL Collective and is authorized to use the repertoire under the terms of that agreement.

Attributes

NameTypeRequiredDescription
idstringYesThe unique identifier for the licensee.
namestringYesDisplay name of the licensee.
urlstringYesOfficial website URL of the licensee. Must be a valid URL.
statusenumYesThe current status of the licensee: active | inactive.

Retrieve a licensee

Returns the licensee object for the specified id.

Endpoint

http
GET /licensees/{id}

Path parameters

NameTypeRequiredDescription
idstringYesid of the licensee object to retrieve.

Response fields

NameTypeDescription
licenseeobjectLicensee object for the requested licensee.

Example response

json
{
  "licensee": {
    "id": "lic_ai_lab_001",
    "name": "Example AI Lab 1 Name",
    "url": "https://example.com",
    "status": "active"
  }
}

List all licensees

Returns a list of licensee objects. Partners can use this list to show publishers which licensees are authorized under the RSL Collective License and let publishers specify which licensees to exclude from accessing their content scopes.

Endpoint

http
GET /licensees

Query parameters

NameTypeRequiredDescription
limitintegerNoMaximum number of licensee objects to return.
starting_afterstringNoid of the last licensee object from the previous page.

Response fields

NameTypeDescription
licenseesarray of objectsList of licensees.
has_morebooleanWhether additional licensee objects are available.

Example response

json
{
  "licensees": [
    {
      "licensee": {
        "id": "lic_ai_lab_001",
        "name": "Example AI Lab 1 Name",
        "url": "https://example.com",
        "status": "active"
      }
    },
    {
      "licensee": {
        "id": "lic_ai_lab_002",
        "name": "Example AI Lab 2 Name",
        "url": "https://example.org",
        "status": "active"
      }
    }
  ],
  "has_more": false
}

The Repertoire object

A repertoire object represents the complete set of content scopes made available by a partner's publishers under the terms of the RSL Collective license agreement.

The repertoire object uses a full repertoire model rather than incremental updates. This improves reconciliation, avoids synchronization errors from missed or out-of-order updates, and simplifies large-scale integrations.

Attributes

NameTypeRequiredDescription
uploadobjectYesUpload object for the corresponding repertoire file.

Create a repertoire

To submit a repertoire file, a partner creates a repertoire object. The request returns a repertoire object containing the upload object for that file.

After the repertoire file is uploaded and validated successfully, it becomes the active repertoire and replaces the previous active repertoire. Rows rejected with conflicting_scope_url are not included in the active repertoire.

Endpoint

http
POST /repertoires

Body parameters

NameTypeRequiredDescription
fileobjectYesFile metadata for the corresponding repertoire file.

Response fields

NameTypeDescription
repertoireobjectRepertoire object for the upload workflow.

Example request

json
{
  "file": {
    "format": "csv",
    "schema_version": "1.0",
    "compression": "gzip",
    "size": 2096128,
    "sha256": "d26b94ae60e4554c7333034960d2146f3801f42a7516a566a014a2f1e6d9c3b7",
    "validate_only": false
  }
}

Example response

json
{
  "repertoire": {
    "upload": {
      "id": "job_9aK2mQ7xT4vN8pR3cW6yZ1bD",
      "status": "ready",
      "created": 1760000000,
      "updated": null,
      "completed": null,
      "result_url": null,
      "result_url_expires": null,
      "result_sha256": null,
      "file": {
        "id": "file_f7Lp2Vx9qM4nT8rC3wK6yH1d",
        "created": 1760000000,
        "format": "csv",
        "schema_version": "1.0",
        "compression": "gzip",
        "size": 2096128,
        "sha256": "d26b94ae60e4554c7333034960d2146f3801f42a7516a566a014a2f1e6d9c3b7",
        "url": "https://files.rslcollective.org/file_f7Lp2Vx9qM4nT8rC3wK6yH1d",
        "url_expires": 1760086400,
        "validate_only": false
      }
    }
  }
}

Conflicting content scope enrollments

A content scope can be enrolled through only one partner at a time, and within a single repertoire file, each scope_url must appear only once. If the same scope_url appears more than once in the uploaded file, the upload fails.

If the same canonical scope_url is submitted through more than one partner, the Enrollment API compares rights_attestation_date and applies the most recent submission. If two submissions have the same rights_attestation_date, the submission already in the active repertoire remains in effect. Any other conflicting submission is rejected and not applied.

Rejecting one or more conflicting rows does not by itself cause the upload to fail.

Retrieve a repertoire

Returns the repertoire object for the specified id. Partners can use this endpoint to monitor the status of uploading and validating a repertoire file to create a repertoire object.

When the upload object reaches a terminal state, the upload.result_url field provides a temporary presigned URL for downloading the corresponding repertoire result file.

Endpoint

http
GET /repertoires/{id}

Path parameters

NameTypeRequiredDescription
idstringYesupload.id of the repertoire object to retrieve.

Response fields

NameTypeDescription
repertoireobjectRepertoire object for the requested repertoire.

Example response

json
{
  "repertoire": {
    "upload": {
      "id": "job_9aK2mQ7xT4vN8pR3cW6yZ1bD",
      "status": "failed",
      "created": 1760000000,
      "updated": 1760001200,
      "completed": 1760001200,
      "result_url": "https://files.rslcollective.org/result_M4xq8Rk2Vn7pT5cY9wL1bD6h.json",
      "result_url_expires": 1760087600,
      "result_sha256": "f26b94ae60e4554c7333034960d2146f3801f42a7516a566a014a2f1e6d9c3b9",
      "file": {
        "id": "file_f7Lp2Vx9qM4nT8rC3wK6yH1d",
        "created": 1760000000,
        "format": "csv",
        "schema_version": "1.0",
        "compression": "gzip",
        "size": 2096128,
        "sha256": "d26b94ae60e4554c7333034960d2146f3801f42a7516a566a014a2f1e6d9c3b7",
        "url": null,
        "url_expires": null,
        "validate_only": false
      }
    }
  }
}

Repertoire result file

Repertoire uploads return a standard result file. The following additional row-level error_code values may appear for repertoire uploads:

ValueDescriptionAction
conflicting_scope_urlscope_url conflicts with an active enrollment through another partner that has a more recent rights_attestation_date.Row skipped

Only conflicting_scope_url is skippable. The other repertoire-specific row-level errors cause the job to fail.

Success result example
json
{
  "result": {
    "id": "job_9aK2mQ7xT4vN8pR3cW6yZ1bD",
    "status": "succeeded",
    "rows_processed": 48280,
    "rows_skipped": 1,
    "errors": [
      {
        "row_number": 14,
        "column": "scope_url",
        "error_code": "conflicting_scope_url",
        "error_description": "`scope_url` conflicts with an active enrollment through another partner that has a more recent `rights_attestation_date`."
      }
    ]
  }
}
Failed result example
json
{
  "result": {
    "id": "job_9aK2mQ7xT4vN8pR3cW6yZ1bD",
    "status": "failed",
    "error_code": "validation_failed",
    "rows_processed": 13,
    "rows_skipped": 0,
    "errors": [
      {
        "row_number": 14,
        "column": "scope_url",
        "error_code": "duplicate_key",
        "error_description": "scope_url appears more than once in the uploaded file."
      }
    ]
  }
}

List all repertoires

Returns a list of available repertoire objects in reverse chronological order. The most recent repertoire object whose upload.file.validate_only is false and whose upload.status is succeeded is the repertoire of record for operational use.

To retrieve a specific repertoire object directly, use GET /repertoires/{id}.

Endpoint

http
GET /repertoires

Query parameters

NameTypeRequiredDescription
limitintegerNoMaximum number of repertoire objects to return.
starting_afterstringNoupload.id of the last repertoire object from the previous page.

Response fields

NameTypeDescription
repertoiresarray of objectsList of repertoire objects. Each array item contains a repertoire object.
has_morebooleanWhether additional repertoire objects are available.

Example response

json
{
  "repertoires": [
    {
      "repertoire": {
        "upload": {
          "id": "job_9aK2mQ7xT4vN8pR3cW6yZ1bD",
          "status": "processing",
          "created": 1760002000,
          "updated": 1760002060,
          "completed": null,
          "result_url": null,
          "result_url_expires": null,
          "result_sha256": null,
          "file": {
            "id": "file_f7Lp2Vx9qM4nT8rC3wK6yH1d",
            "created": 1760002000,
            "format": "csv",
            "schema_version": "1.0",
            "compression": "gzip",
            "size": 2096128,
            "sha256": "d26b94ae60e4554c7333034960d2146f3801f42a7516a566a014a2f1e6d9c3b7",
            "url": null,
            "url_expires": null,
            "validate_only": false
          }
        }
      }
    },
    {
      "repertoire": {
        "upload": {
          "id": "job_M4xq8Rk2Vn7pT5cY9wL1bD6h",
          "status": "succeeded",
          "created": 1760000000,
          "updated": 1760001200,
          "completed": 1760001200,
          "result_url": "https://files.rslcollective.org/result_M4xq8Rk2Vn7pT5cY9wL1bD6h.json",
          "result_url_expires": 1760087600,
          "result_sha256": "f26b94ae60e4554c7333034960d2146f3801f42a7516a566a014a2f1e6d9c3b9",
          "file": {
            "id": "file_P7mT3vX9qR4nK8yC2wL6dH1z",
            "created": 1760000000,
            "format": "csv",
            "schema_version": "1.0",
            "compression": "gzip",
            "size": 2096128,
            "sha256": "d26b94ae60e4554c7333034960d2146f3801f42a7516a566a014a2f1e6d9c3b7",
            "url": null,
            "url_expires": null,
            "validate_only": false
          }
        }
      }
    }
  ],
  "has_more": false
}

Repertoire file

A Repertoire file represents the complete repertoire of a partner. The same publisher can appear in multiple rows, and each row defines one licensed content scope.

Repertoire CSV files must be UTF-8 encoded and include a header row. The header row must contain the required column names defined below, and columns may appear in any order.

ColumnTypeRequiredLimitDescription
publisher_idstringYes40 charsStable partner-defined identifier for the publisher.
publisher_urlstringYes512 charsCanonical website URL representing the publisher. This field is informational.
enrollment_attestation_datetimestampYes10 digitsUTC Unix timestamp when the Enrollment Attestation was executed or recorded by the partner.
enrollment_attestation_idstringYes40 charsStable partner-defined identifier for the Enrollment Attestation record.
rights_attestation_datetimestampYes10 digitsUTC Unix timestamp when the Rights Attestation authorizing licensing of the enrolled content scope was executed or recorded by the partner.
rights_attestation_idstringYes40 charsStable partner-defined identifier for the Rights Attestation record.
scope_urlstringYes512 charsURL representing the licensed content scope governed by an RSL license declaration published under the RSL Collective License.
exclusionsstringNo1024 chars totalOptional semicolon-delimited (;) list of licensee IDs excluded from licensing this content scope.

Partner-defined identifiers in publisher_id, enrollment_attestation_id, and rights_attestation_id must not contain carriage return, line feed, or NULL characters.

Each licensee ID included in exclusions must be no more than 40 characters. exclusions must use semicolon (;) as the delimiter and must not contain empty items.

Example repertoire file

csv
publisher_id,publisher_url,enrollment_attestation_date,enrollment_attestation_id,rights_attestation_date,rights_attestation_id,scope_url,exclusions
pub_001,https://example.com,1760000000,pub_att_1001,1760000100,rights_att_2001,https://example.com/,
pub_001,https://example.com,1760000000,pub_att_1001,1760000200,rights_att_2002,https://example.com/feed.xml,lic_ai_lab_002
pub_002,https://news.example.org,1760000300,pub_att_3001,1760000400,rights_att_4001,https://news.example.org/,

Content scope URL canonicalization

For overlap validation and conflict detection, scope_url values are canonicalized using the WHATWG URL Standard. The canonicalized scope_url preserves the scheme, host, path, and query. A single leftmost www. label is removed from the host, and fragment identifiers are ignored.

For example, https://www.example.com/ and https://example.com/ are treated as the same scope_url. Other host labels, such as www2.example.com and www.subdomain.example.com, remain distinct.

Partners should normalize scope_url values using the same rules before submission.

The Report object

A report object represents the licensee repertoire usage data and payment proceeds for a reporting period, and includes the metadata needed to download the corresponding report file.

Attributes

NameTypeRequiredDescription
period_startstring (YYYY-MM-DD)YesInclusive UTC start date of the reporting period.
period_endstring (YYYY-MM-DD)YesInclusive UTC end date of the reporting period.
fileobjectYesFile object for the corresponding report file.

Retrieve a report

Returns the report object for the specified id. The report object includes a temporary presigned file.url for downloading the corresponding report file.

Endpoint

http
GET /reports/{id}

Path parameters

NameTypeRequiredDescription
idstringYesfile.id of the report object to retrieve.

Response fields

NameTypeDescription
reportobjectReport object for the requested report.

Example response

json
{
  "report": {
    "period_start": "2025-04-04",
    "period_end": "2025-04-04",
    "file": {
      "id": "file_f7Lp2Vx9qM4nT8rC3wK6yH1d",
      "created": 1759160000,
      "format": "csv",
      "schema_version": "1.0",
      "compression": "gzip",
      "size": 1048576,
      "sha256": "d26b94ae60e4554c7333034960d2146f3801f42a7516a566a014a2f1e6d9c3b7",
      "url": "https://files.rslcollective.org/file_f7Lp2Vx9qM4nT8rC3wK6yH1d.csv.gz",
      "url_expires": 1759160400,
      "validate_only": false
    }
  }
}

List all reports

Returns a list of available report objects in reverse chronological order. For a given reporting period, the report object with the most recent file.created timestamp is the current report of record. When a new report is issued, it supersedes the previous report of record for that reporting period.

To retrieve a specific report object directly, use GET /reports/{id}.

Endpoint

http
GET /reports

Query parameters

NameTypeRequiredDescription
limitintegerNoMaximum number of report objects to return.
starting_afterstringNofile.id of the last report object from the previous page.

Response fields

NameTypeDescription
reportsarray of objectsList of report objects. Each array item contains a report object.
has_morebooleanWhether additional report objects are available.

Example response

json
{
  "reports": [
    {
      "report": {
        "period_start": "2025-04-05",
        "period_end": "2025-04-05",
        "file": {
          "id": "file_f7Lp2Vx9qM4nT8rC3wK6yH1d",
          "created": 1759160000,
          "format": "csv",
          "schema_version": "1.0",
          "compression": "gzip",
          "size": 1048576,
          "sha256": "8f3c1a7d4b92e6f0c5a8d1e3b7f4092c6e1a5d8f3b0c7e2d9a4f1c6b8e3d7a5",
          "url": "https://files.rslcollective.org/file_f7Lp2Vx9qM4nT8rC3wK6yH1d.csv.gz",
          "url_expires": 1759160400,
          "validate_only": false
        }
      }
    },
    {
      "report": {
        "period_start": "2025-04-04",
        "period_end": "2025-04-04",
        "file": {
          "id": "file_P7mT3vX9qR4nK8yC2wL6dH1z",
          "created": 1759070000,
          "format": "csv",
          "schema_version": "1.0",
          "compression": "gzip",
          "size": 3128576,
          "sha256": "b83c1f4e92d7a6c5e1f0438b7c2d9e6a4f1b8c7d5e2a9f3c6b1d4e7f8a2c5d9",
          "url": "https://files.rslcollective.org/file_P7mT3vX9qR4nK8yC2wL6dH1z.csv.gz",
          "url_expires": 1759074000,
          "validate_only": false
        }
      }
    }
  ],
  "has_more": false
}

Report file

This report file schema is provisional and may change before general availability. Report CSV files must be UTF-8 encoded and include a header row. The header row must contain the required column names defined below, and columns may appear in any order.

ColumnTypeRequiredLimitDescription
licensee_idstringYes40 charsIdentifier of the licensee that reported the usage.
report_datestring (YYYY-MM-DD)Yes10 charsUTC date covered by the reported usage and payment data.
publisher_idstringYes40 charsStable partner-defined identifier for the publisher.
scope_urlstringYes512 charsURL representing the licensed content scope.
usage_countintegerYes64-bit integerNumber of reported usage events for the content scope on the specified report date. Must be zero or greater.
payment_amountintegerYes64-bit integerPayment amount allocated to the content scope for the specified report date, expressed in the smallest unit of payment_currency (for example, cents for USD and whole yen for JPY).
payment_currencystringYes3 charsThree-letter ISO 4217 currency code for the payment amount (for example USD).

Example report file

In this example, payment_amount is expressed in the smallest unit of payment_currency, so 4250 means USD 42.50.

csv
licensee_id,report_date,publisher_id,scope_url,usage_count,payment_amount,payment_currency
lic_ai_lab_001,2026-03-01,pub_001,https://example.com/,125,4250,USD
lic_ai_lab_001,2026-03-01,pub_001,https://example.com/feed.xml,18,675,USD
lic_ai_lab_002,2026-03-01,pub_002,https://news.example.org/,203,7120,USD

The IndexNow object

An indexnow object lets partners notify licensees of URL-level changes to the enrollment repertoire within defined service-level requirements, rather than waiting for licensees to detect those changes through regular re-fetching of licensed content scopes. Each change notification tells licensees that a specific URL within a previously declared content scope has been added, updated, deleted, or removed, and follows the conventions of the IndexNow protocol.

An indexnow object provides incremental, ordered updates to URLs within content scopes already present in the partner’s current effective repertoire. It does not declare new content scopes or replace the repertoire.

Attributes

NameTypeRequiredDescription
uploadobjectYesUpload object for the corresponding indexnow file.

Create an IndexNow

To submit an indexnow file, a partner creates an indexnow object. The request returns an indexnow object containing the upload object for that file.

After the indexnow file is uploaded and validated successfully, its change notifications become active and must be applied by licensees in the order received.

Endpoint

http
POST /indexnows

Body parameters

NameTypeRequiredDescription
fileobjectYesFile metadata for the corresponding indexnow file.

Response fields

NameTypeDescription
indexnowobjectIndexNow object for the upload workflow.

Example request

json
{
  "file": {
    "schema_version": "1.0",
    "format": "csv",
    "compression": "gzip",
    "size": 2096128,
    "sha256": "d26b94ae60e4554c7333034960d2146f3801f42a7516a566a014a2f1e6d9c3b7"
  }
}

Example response

json
{
  "indexnow": {
    "upload": {
      "id": "job_9aK2mQ7xT4vN8pR3cW6yZ1bD",
      "status": "ready",
      "created": 1760000000,
      "updated": null,
      "completed": null,
      "result_url": null,
      "result_url_expires": null,
      "result_sha256": null,
      "file": {
        "id": "file_f7Lp2Vx9qM4nT8rC3wK6yH1d",
        "created": 1760000000,
        "format": "csv",
        "schema_version": "1.0",
        "compression": "gzip",
        "size": 2096128,
        "sha256": "d26b94ae60e4554c7333034960d2146f3801f42a7516a566a014a2f1e6d9c3b7",
        "url": "https://files.rslcollective.org/file_f7Lp2Vx9qM4nT8rC3wK6yH1d",
        "url_expires": 1760086400,
        "validate_only": false
      }
    }
  }
}

Retrieve an IndexNow

Returns the indexnow object for the specified id. Partners can use this endpoint to monitor the status of uploading and validating an indexnow file to create an indexnow object.

When the upload object reaches a terminal state, the upload.result_url field provides a temporary presigned URL for downloading the corresponding indexnow result file.

Endpoint

http
GET /indexnows/{id}

Path parameters

NameTypeRequiredDescription
idstringYesupload.id of the IndexNow object to retrieve.

Response fields

NameTypeDescription
indexnowobjectIndexNow object for the requested indexnow file.

Example response

json
{
  "indexnow": {
    "upload": {
      "id": "job_9aK2mQ7xT4vN8pR3cW6yZ1bD",
      "status": "failed",
      "created": 1760000000,
      "updated": 1760001200,
      "completed": 1760001200,
      "result_url": "https://files.rslcollective.org/result_9aK2mQ7xT4vN8pR3cW6yZ1bD.json",
      "result_url_expires": 1760087600,
      "result_sha256": "f26b94ae60e4554c7333034960d2146f3801f42a7516a566a014a2f1e6d9c3b9",
      "file": {
        "id": "file_f7Lp2Vx9qM4nT8rC3wK6yH1d",
        "created": 1760000000,
        "format": "csv",
        "schema_version": "1.0",
        "compression": "gzip",
        "size": 2096128,
        "sha256": "d26b94ae60e4554c7333034960d2146f3801f42a7516a566a014a2f1e6d9c3b7",
        "url": null,
        "url_expires": null,
        "validate_only": false
      }
    }
  }
}

IndexNow result file

Indexnow uploads return a standard result file. The following additional row-level error_code values may appear for indexnow uploads:

ValueDescription
url_outside_scopeurl does not fall within the declared scope_url.

These indexnow-specific row-level errors cause the job to fail.

Failed result example
json
{
  "result": {
    "id": "job_01JV1N0A9Q6M2X7P4R8T3K5N1C",
    "status": "failed",
    "error_code": "validation_failed",
    "rows_processed": 13,
    "rows_skipped": 0,
    "errors": [
      {
        "row_number": 14,
        "column": "url",
        "error_code": "url_outside_scope",
        "error_description": "url does not fall within the declared scope_url."
      }
    ]
  }
}

List all IndexNows

Returns a list of available indexnow objects in reverse chronological order.

To retrieve a specific indexnow object directly, use GET /indexnow/{id}.

Endpoint

http
GET /indexnows

Query parameters

NameTypeRequiredDescription
limitintegerNoMaximum number of indexnow objects to return.
starting_afterstringNoupload.id of the last indexnow object from the previous page.

Response fields

NameTypeDescription
indexnowsarray of objectsList of indexnow objects. Each array item contains an indexnow object.
has_morebooleanWhether additional indexnow objects are available.

Example response

json
{
  "indexnows": [
    {
      "indexnow": {
        "upload": {
          "id": "job_9aK2mQ7xT4vN8pR3cW6yZ1bD",
          "status": "processing",
          "created": 1760002000,
          "updated": 1760002060,
          "completed": null,
          "result_url": null,
          "result_url_expires": null,
          "result_sha256": null,
          "file": {
            "id": "file_f7Lp2Vx9qM4nT8rC3wK6yH1d",
            "created": 1760002000,
            "format": "csv",
            "schema_version": "1.0",
            "compression": "gzip",
            "size": 2096128,
            "sha256": "d26b94ae60e4554c7333034960d2146f3801f42a7516a566a014a2f1e6d9c3b7",
            "url": null,
            "url_expires": null,
            "validate_only": false
          }
        }
      }
    },
    {
      "indexnow": {
        "upload": {
          "id": "job_M4xq8Rk2Vn7pT5cY9wL1bD6h",
          "status": "succeeded",
          "created": 1760000000,
          "updated": 1760001200,
          "completed": 1760001200,
          "result_url": "https://files.rslcollective.org/result_M4xq8Rk2Vn7pT5cY9wL1bD6h.json",
          "result_url_expires": 1760087600,
          "result_sha256": "f26b94ae60e4554c7333034960d2146f3801f42a7516a566a014a2f1e6d9c3b9",
          "file": {
            "id": "file_P7mT3vX9qR4nK8yC2wL6dH1z",
            "created": 1760000000,
            "format": "csv",
            "schema_version": "1.0",
            "compression": "gzip",
            "size": 2096128,
            "sha256": "d26b94ae60e4554c7333034960d2146f3801f42a7516a566a014a2f1e6d9c3b7",
            "url": null,
            "url_expires": null,
            "validate_only": false
          }
        }
      }
    }
  ],
  "has_more": false
}

IndexNow file

An indexnow file defines URL-level changes within previously declared content scopes. Each row defines the current change for one URL within a licensed content scope. Rows in an indexnow file are ordered by change time, and must be processed sequentially from top to bottom to apply changes in the correct order.

Indexnow CSV files must be UTF-8 encoded and include a header row. The header row must contain the required column names defined below, and columns may appear in any order.

ColumnTypeRequiredLimitDescription
publisher_idstringYes40 charsStable partner-defined identifier for the publisher.
publisher_urlstringYes512 charsCanonical website URL representing the publisher. This field is informational.
scope_urlstringYes512 charsCanonical URL of the declared content scope containing the URL.
urlstringYes512 charsCanonical URL whose change is being reported.
changeenumYes16 charsadded, updated, deleted, or removed.
timestampintegerYes10 charsUTC Unix timestamp when the URL-level change occurred.

Change values

ChangeDescription
addedThe URL is newly available within an existing declared content scope.
updatedThe URL remains available within the declared content scope, but its content has changed materially.
deletedThe URL is no longer available due to an ordinary deletion.
removedThe URL is no longer available due to a rights, legal, safety, or compliance-driven removal.

Example IndexNow file

csv
publisher_id,publisher_url,scope_url,url,change,timestamp
pub_001,https://example.com,https://example.com/,https://example.com/articles/example-1,removed,1774223900
pub_001,https://example.com,https://example.com/,https://example.com/articles/example-2,added,1774223940
pub_001,https://example.com,https://example.com/,https://example.com/articles/example-3,updated,1774223970
pub_001,https://example.com,https://example.com/,https://example.com/articles/example-3,deleted,1774223995

Appendix: Machine-readable schemas and integration artifacts

This appendix defines machine-readable artifacts for the RSL Platform Enrollment API, including:

  • an OpenAPI definition for JSON-based API endpoints
  • machine-readable schemas for the repertoire file, indexnow file, and report file CSV formats
  • a machine-readable error catalog

Notes:

  • The OpenAPI definition describes the machine-readable definition for JSON-based API endpoints and JSON response objects. It does not model CSV row schemas
  • The YAML CSV schema for the repertoire file describes the machine-readable definition for that format.
  • The YAML CSV schema for the indexnow file describes the machine-readable definition for that format.
  • The YAML CSV schema for the report file is provisional and provided for planning purposes until licensee reporting obligations are finalized.

OpenAPI definition

yaml
openapi: 3.1.0
info:
  title: RSL Platform Enrollment API
  version: 1.0.0-beta
  description: >
    Machine-readable definition of the RSL Platform Enrollment API.

servers:
  - url: https://api.rslcollective.org/enrollment/v1
    description: Production
  - url: https://sandbox.api.rslcollective.org/enrollment/v1
    description: Sandbox

security:
  - bearerAuth: []

paths:
  /licensees:
    get:
      summary: List all licensees
      operationId: listLicensees
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 100
            minimum: 1
            maximum: 1000
        - name: starting_after
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Successful response
          headers:
            Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LicenseeListResponse"

  /licensees/{id}:
    get:
      summary: Retrieve a licensee
      operationId: getLicensee
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Successful response
          headers:
            Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LicenseeObject"
        "404":
          $ref: "#/components/responses/Error"

  /repertoires:
    post:
      summary: Create a repertoire
      operationId: createRepertoire
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RepertoireRequest"
      responses:
        "200":
          description: Repertoire object created
          headers:
            Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RepertoireObject"
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Error"
        "403":
          $ref: "#/components/responses/Error"
        "409":
          $ref: "#/components/responses/Error"
        "429":
          $ref: "#/components/responses/Error"

    get:
      summary: List all repertoires
      operationId: listRepertoires
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 100
            minimum: 1
            maximum: 1000
        - name: starting_after
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Successful response
          headers:
            Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RepertoireListResponse"

  /repertoires/{id}:
    get:
      summary: Retrieve a repertoire
      operationId: getRepertoire
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Successful response
          headers:
            Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RepertoireObject"
        "404":
          $ref: "#/components/responses/Error"

  /reports:
    get:
      summary: List all reports
      operationId: listReports
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 100
            minimum: 1
            maximum: 1000
        - name: starting_after
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Successful response
          headers:
            Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReportListResponse"

  /reports/{id}:
    get:
      summary: Retrieve a report
      operationId: getReport
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Successful response
          headers:
            Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReportObject"
        "404":
          $ref: "#/components/responses/Error"

  /indexnows:
    post:
      summary: Create an IndexNow
      operationId: createIndexNow
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IndexNowRequest"
      responses:
        "200":
          description: Indexnow object created
          headers:
            Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IndexNowObject"
        "400":
          $ref: "#/components/responses/Error"
        "401":
          $ref: "#/components/responses/Error"
        "403":
          $ref: "#/components/responses/Error"
        "409":
          $ref: "#/components/responses/Error"
        "429":
          $ref: "#/components/responses/Error"

    get:
      summary: List all IndexNows
      operationId: listIndexNows
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            default: 100
            minimum: 1
            maximum: 1000
        - name: starting_after
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Successful response
          headers:
            Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IndexNowListResponse"

  /indexnows/{id}:
    get:
      summary: Retrieve an IndexNow
      operationId: getIndexNow
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Successful response
          headers:
            Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IndexNowObject"
        "404":
          $ref: "#/components/responses/Error"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API key

  headers:
    RequestId:
      description: Unique request identifier
      schema:
        type: string

  responses:
    Error:
      description: Error response
      headers:
        Request-Id:
          $ref: "#/components/headers/RequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

  schemas:
    Error:
      type: object
      required: [error, error_description]
      properties:
        error:
          type: string
        error_description:
          type: string

    Licensee:
      type: object
      description: Inner licensee resource object.
      required: [id, name, url, status]
      properties:
        id:
          type: string
        name:
          type: string
        url:
          type: string
          format: uri
        status:
          type: string
          enum: [active, inactive]

    LicenseeObject:
      type: object
      description: Response wrapper containing a single licensee object.
      required: [licensee]
      properties:
        licensee:
          $ref: "#/components/schemas/Licensee"

    LicenseeListResponse:
      type: object
      description: Paginated list response containing licensee objects.
      required: [licensees, has_more]
      properties:
        licensees:
          type: array
          items:
            $ref: "#/components/schemas/LicenseeObject"
        has_more:
          type: boolean

    RepertoireRequest:
      type: object
      required: [file]
      properties:
        file:
          $ref: "./files.openapi.yaml#/components/schemas/FileInput"

    Repertoire:
      type: object
      description: Inner repertoire resource object.
      required: [upload]
      properties:
        upload:
          $ref: "./files.openapi.yaml#/components/schemas/Upload"

    RepertoireObject:
      type: object
      description: Response wrapper containing a single repertoire object.
      required: [repertoire]
      properties:
        repertoire:
          $ref: "#/components/schemas/Repertoire"

    RepertoireListResponse:
      type: object
      description: Paginated list response containing repertoire objects.
      required: [repertoires, has_more]
      properties:
        repertoires:
          type: array
          items:
            $ref: "#/components/schemas/RepertoireObject"
        has_more:
          type: boolean

    Report:
      type: object
      description: Inner report resource object.
      required: [period_start, period_end, file]
      properties:
        period_start:
          type: string
          format: date
        period_end:
          type: string
          format: date
        file:
          $ref: "./files.openapi.yaml#/components/schemas/File"

    ReportObject:
      type: object
      description: Response wrapper containing a single report object.
      required: [report]
      properties:
        report:
          $ref: "#/components/schemas/Report"

    ReportListResponse:
      type: object
      description: Paginated list response containing report objects.
      required: [reports, has_more]
      properties:
        reports:
          type: array
          items:
            $ref: "#/components/schemas/ReportObject"
        has_more:
          type: boolean

    IndexNowRequest:
      type: object
      required: [file]
      properties:
        file:
          $ref: "./files.openapi.yaml#/components/schemas/FileInput"

    IndexNow:
      type: object
      description: Inner indexnow resource object.
      required: [upload]
      properties:
        upload:
          $ref: "./files.openapi.yaml#/components/schemas/Upload"

    IndexNowObject:
      type: object
      description: Response wrapper containing a single indexnow object.
      required: [indexnow]
      properties:
        indexnow:
          $ref: "#/components/schemas/IndexNow"

    IndexNowListResponse:
      type: object
      description: Paginated list response containing indexnow objects.
      required: [indexnow, has_more]
      properties:
        indexnow:
          type: array
          items:
            $ref: "#/components/schemas/IndexNowObject"
        has_more:
          type: boolean

Repertoire file CSV schema

yaml
schema_type: rsl.enrollment_api.csv_schema
schema_version: "1.0"
file_type: repertoire_file
encoding: UTF-8
header_required: true
unknown_columns: forbidden
duplicate_columns: forbidden
columns_may_appear_in_any_order: true

columns:
  - name: publisher_id
    type: string
    required: true
    max_length: 40
    description: Stable partner-defined identifier for the publisher.

  - name: publisher_url
    type: string
    format: uri
    required: true
    max_length: 512
    description: Canonical website URL representing the publisher. This field is informational.

  - name: enrollment_attestation_date
    type: integer
    required: true
    minimum: 0
    maximum: 9999999999
    description: UTC Unix timestamp when the Enrollment Attestation was executed or recorded by the partner.

  - name: enrollment_attestation_id
    type: string
    required: true
    max_length: 40
    description: Stable partner-defined identifier for the Enrollment Attestation record.

  - name: rights_attestation_date
    type: integer
    required: true
    minimum: 0
    maximum: 9999999999
    description: UTC Unix timestamp when the Rights Attestation authorizing licensing of the enrolled content scope was executed or recorded by the partner.

  - name: rights_attestation_id
    type: string
    required: true
    max_length: 40
    description: Stable partner-defined identifier for the Rights Attestation record.

  - name: scope_url
    type: string
    format: uri
    required: true
    max_length: 512
    description: URL representing the licensed content scope governed by an RSL license declaration published under the RSL Collective License.

  - name: exclusions
    type: string
    required: false
    max_length: 1024
    list:
      delimiter: ";"
      item_type: string
      item_max_length: 40
      allow_empty_items: false
    description: Optional semicolon-delimited list of licensee IDs excluded from licensing this content scope.

constraints:
  uniqueness:
    - [scope_url]
  forbidden_characters:
    publisher_id: ["\r", "\n", "\u0000"]
    enrollment_attestation_id: ["\r", "\n", "\u0000"]
    rights_attestation_id: ["\r", "\n", "\u0000"]

validation_errors:
  - conflicting_scope_url

Report file CSV schema

yaml
schema_type: rsl.enrollment_api.csv_schema
schema_version: "1.0"
file_type: report_file
encoding: UTF-8
header_required: true
unknown_columns: forbidden
duplicate_columns: forbidden
columns_may_appear_in_any_order: true
status: placeholder
status_note: >
  The report file schema is provisional and may change before general availability.

columns:
  - name: licensee_id
    type: string
    required: true
    max_length: 40
    description: Identifier of the licensee that reported the usage.

  - name: report_date
    type: string
    format: date
    required: true
    description: UTC date covered by the reported usage and payment data.

  - name: publisher_id
    type: string
    required: true
    max_length: 40
    description: Stable partner-defined identifier for the publisher.

  - name: scope_url
    type: string
    format: uri
    required: true
    max_length: 512
    description: URL representing the licensed content scope.

  - name: usage_count
    type: integer
    required: true
    minimum: 0
    description: Number of reported usage events for the content scope on the specified report date.

  - name: payment_amount
    type: integer
    required: true
    minimum: 0
    description: Payment amount allocated to the content scope for the specified report date, expressed in the smallest unit of payment_currency.

  - name: payment_currency
    type: string
    required: true
    pattern: "^[A-Z]{3}$"
    max_length: 3
    description: Three-letter ISO 4217 currency code for the payment amount.

constraints: {}

IndexNow file CSV schema

yaml
schema_type: rsl.enrollment_api.csv_schema
schema_version: "1.0"
file_type: indexnow_file
encoding: UTF-8
header_required: true
unknown_columns: forbidden
duplicate_columns: forbidden
columns_may_appear_in_any_order: true

columns:
  - name: publisher_id
    type: string
    required: true
    max_length: 40
    description: Stable partner-defined identifier for the publisher.

  - name: publisher_url
    type: string
    format: uri
    required: true
    max_length: 512
    description: Canonical website URL representing the publisher. This field is informational.

  - name: scope_url
    type: string
    format: uri
    required: true
    max_length: 512
    description: Canonical URL of the declared content scope containing the URL.

  - name: url
    type: string
    format: uri
    required: true
    max_length: 512
    description: Canonical URL whose change is being reported.

  - name: change
    type: string
    required: true
    max_length: 16
    enum: [added, updated, deleted, removed]
    description: Type of URL-level change.

  - name: timestamp
    type: integer
    required: true
    minimum: 0
    maximum: 9999999999
    description: UTC Unix timestamp when the URL-level change occurred. 

constraints: {}

validation_errors:
  - url_outside_scope

Error catalog

This catalog defines the Enrollment API-specific machine-readable error codes used by this API in addition to the shared file-level and row-level error codes defined by the Files API. API error responses use the generic Error schema defined in the OpenAPI definition.

yaml
catalog_type: rsl.enrollment_api.error_catalog
catalog_version: 1.0.0-beta

repertoire_validation_errors:
  - code: conflicting_scope_url
    category: repertoire_row
    retryable: false
    description: "`scope_url` conflicts with an active enrollment through another partner that has a more recent `rights_attestation_date`."

indexnow_validation_errors:
  - code: url_outside_scope
    category: indexnow_row
    retryable: false
    description: "`url` does not fall within the declared `scope_url`."

Changelog

  • 2026-03-31: Updated the Enrollment API to use the current Files API shared objects and workflow patterns.
  • 2026-03-06: Initial Enrollment API.