, , ,

TM Forum API Extensions: Where the Extra Field Is Allowed to Live

Jane is already in the system. The last post made her a person, then a buyer, and left the record plain on purpose. The form still has no box for a loyalty tier or an old account number. This post is about the two honest places that extra fact can go, how another system hears that she was created, and which machine is allowed to send the change. We are not selling her a plan here.

@type on that plain record said Individual, then Customer, so the receiver knew which shape it had been given. That label was not permission to invent a field. TM Forum API extensions are how the payload grows after those two creates in A TM Forum API, Written Out. You need that pair already: the person on TMF632, the buyer on TMF629, the href kept. The tier and the old subscriber id are the Monday ticket. They are still not fields on the individual spec.


At a Glance

  • A characteristic holds a fact, such as a loyalty tier. @type stays Individual. A schema extension is a new type: your name in @type, Individual in @baseType, and a schema you can point at.
  • TMF688 is how another system hears the create. You register a callback. The publisher POSTs the event. The listener keeps her address, the href, so it can open her record later.
  • OAuth 2.0 is how the machine is allowed to send the body. OpenID Connect is for a person in the loop. Intent, TMF921, describes an outcome and then this post stops.

The field that is not on the individual

Monday’s ticket is short. The CRM still has a loyalty tier, or it still has SubscriberId, and someone wants it on the same JSON both vendors will call TMF632. Last time I showed the failure in one object:

{
  "@type": "Individual",
  "givenName": "Jane",
  "familyName": "Doe",
  "SubscriberId": "SUB-2014-4491"
}

The individual spec does not have SubscriberId. Shoving an unapproved key into the root JSON does not make the mapping done. It breaks schema validation and hides an unmanaged column inside a payload both sides will defend as standard.

Loyalty tier is a fairer request. Jane really does have a tier, and the care screen really does need it. The question is where it lives so a second team can see that it is yours. There are two legal places. A private key with neither of them is the same failure as the subscriber id, written more politely.

We are still not ordering the plan. The product order can wait until the extra fact has an address.


A fact, or a type

TMF630 Part 1 was the ordinary shape: a resource you can say out loud, an id, an href, create and read. Part 2 is the chapter on polymorphism and extension. I am not pinning this page to one release of that document. If the PDF on your desk numbers the attributes differently, use the PDF for the list and keep the distinction.

The characteristic

A characteristic is a name and a value on the resource. Use it when the extra fact is data, and Jane is still an individual. On the party API that list is partyCharacteristic. The word in the meeting is characteristic. The field name is the one in the user guide.

POST /partyManagement/v5/individual

{
  "@type": "Individual",
  "givenName": "Jane",
  "familyName": "Doe",
  "partyCharacteristic": [
    {
      "@type": "StringCharacteristic",
      "name": "loyaltyTier",
      "value": "gold"
    }
  ]
}

@type on the individual is still Individual. The tier sits in the list, with its own type so a receiver knows the value is a string and not a nested object it should expand. (Depending on your generator release, your schema may declare polymorphic subclasses like StringCharacteristic or a generic Characteristic with "valueType": "string"—follow your swagger definition). Your gateway’s prefix and version will differ, the same way they did on the plain create. The resource name is the part two teams have to share.

This is the right place for a tier, a segment flag, a preference the base individual never grew a field for. It is a poor place for a second identity. If SubscriberId goes in this list, the name is still yours, and so is the map. The list does not promote a 2014 column into the standard.

One home. Do not also invent loyaltyTier beside givenName. The care screen will read one and the warehouse file will read the other.

The schema extension

A schema extension is a new type you are willing to publish. @type becomes the name of that type. @baseType stays the type you extended. @schemaLocation is where the extra fields are defined. The mandatory fields of the base stay in the body. You do not get to drop givenName because you have a fancier type.

{
  "@type": "LoyaltyIndividual",
  "@baseType": "Individual",
  "@schemaLocation": "https://example.com/schema/LoyaltyIndividual.yaml",
  "givenName": "Jane",
  "familyName": "Doe",
  "loyaltyTier": "gold"
}

example.com is mine, for the page. Yours has to resolve to a schema your consumers can actually open, with the base individual included and loyaltyTier declared on top. If the path 404s, the validator cannot fetch the definition, and your extension is just a broken payload, not a standard contract.

This is the first time @type means “our kind.” On the plain create it only discriminated Individual from Customer. Here it names a subclass. A client that understands LoyaltyIndividual reads loyaltyTier as a field. A client that only understands Individual still has @baseType to fall back on, and it can ignore the extra field instead of rejecting the person.

Publish this when other systems should code to the shape. A tier that marketing renames every quarter is a characteristic. A party subtype the estate agrees to store is a schema. If you cannot point at the schema, you have a private key with a longer @type.

The conformance kit still checks the profile you point it at. Green on TMF632 does not mean the other vendor implemented LoyaltyIndividual.

Both vendors can extend, then talk as if the extra field were the baseline. That is the TMF622 slide from the Open APIs post, one object earlier. Same number on the whiteboard. Two payloads. The fix is the same size as the problem: say which fields are the individual, and which fields are yours.

Jane as an individual with a loyalty characteristic, then a hub callback carrying IndividualCreateEvent.
Figure: The extra fact stays on the person. The publisher POSTs the create to the listener.

Hearing it, instead of polling

Someone still has to notice that Jane exists. The usual design is a job that asks TMF632 every few minutes and compares the page to yesterday. It works until the interval is wrong and billing swears the customer was never created.

TMF688 defines event management across TM Forum APIs. But you do not need to hunt for a separate, standalone event microservice just to subscribe. Each domain API—here, TMF632 Party Management—exposes its own /hub resource following that exact TMF688 pattern. You register a callback with the domain API that owns the resource. When the record changes, the publisher POSTs an event to that callback. The listener does not poll.

POST /partyManagement/v5/hub

{
  "callback": "https://listener.example.com/party-events"
}

That URL is the listener’s address, not Jane’s href. Store both. The hub id that comes back is how you unregister. If you cannot name the callback, you do not have a subscription. You have a hope that some bus is configured.

The event that follows a successful create looks like this. The id is still the example from the last post. The publisher mints eventId. You do not.

{
  "eventId": "evt-9f3c",
  "eventTime": "2026-09-24T09:12:00Z",
  "eventType": "IndividualCreateEvent",
  "event": {
    "individual": {
      "id": "1142",
      "href": "/partyManagement/v5/individual/1142",
      "@type": "Individual"
    }
  }
}

eventType is the sentence. event is the payload. The href inside it is the address you already learned to keep. A customer create is the same shape on the customer API, with CustomerCreateEvent and the customer’s href, which is still not 1142. If your asset wraps the individual one level differently, follow the asset.

The listener stores the event and the href. It can GET the individual when it needs the tier. It should not GET the collection all afternoon to discover whether the POST happened.

A Kafka topic often sits beside this. The bus can carry the same facts. It is an estate choice. The contract in the guidelines is the hub callback. Matching the string IndividualCreateEvent on a topic does not make the topic TMF688. Say which one you actually run.

Standards are not laws. An estate can emit the event and still keep a nightly file for a system that will not take HTTP. What breaks the handoff is telling both teams they are on TMF688 when only one of them can name the hub.


Who is allowed to send it

The bodies above are useless if anyone who can reach the host can create a person. OAuth 2.0 is the mechanism the Open APIs expect. The caller presents a bearer token. The resource server checks the token and then, only then, accepts the POST.

For the machine that creates Jane, the usual grant is client credentials. The channel, or the migration job, is a client. It sends a bearer token on the individual call. I am not putting a token request on this page. A sample client secret in a blog becomes a secret in a repo. The token request already lives in the identity provider you run.

OpenID Connect is the layer on top when a person is in the loop. An agent in a care screen signs in, and the screen acts for that agent. A migrated party does not need a login screen. Bolting one on makes the batch impersonate someone who went home.

Scopes name the API and the action—like party:individual:write or individual:create. That is a scope. telco-admin is a role from the old portal, pasted into a header. If the same client that creates Jane can also raise every product order, the token is too wide. Split the client that writes parties from the client that writes orders.

I am not inventing a claim inside the token. Security profiles move, and a made-up claim becomes a gateway requirement by Friday. Check the issuer, the audience, and the scope. Leave the rest of the token alone until the profile you certified against names it.

The token does not understand loyalty. It understands whether this client may touch the individual. If only care may set loyaltyTier, enforce that in the component that owns the party.


An outcome, then stop

Sooner or later, an architect in the workshop will suggest bypassing the creates and step-by-step orchestrations entirely: “Why bother with individual endpoints? Let’s just use Intent (TMF921) and let the platform figure it out.”

That is where you stop. There is a place for that kind of request, but it does not belong in Jane’s create. TMF921, intent, is how you describe an outcome and leave the execution steps to something else. “Jane has a working mobile plan” is an intent. It names a target state you want achieved. It does not name TMF620, TMF679, or TMF622, and it certainly does not replace the href you stored.

The steps are still the chain from Open APIs Explained: catalog, qualification, product order, service order, inventory, bill. Intent is what you say when a closed loop is allowed to choose those calls. It is a bad first API for a team that has not agreed where the person ends and the buyer begins.

Autonomous-network levels are a later post. So is the canvas those calls will sit on. CAMARA and Open Gateway expose a thin slice of network capability to developers who will never open your party API. Useful, and not this payload.

If the intent arrives before the individual exists, you are back to a workshop. Create the person. Point the customer at her. Put the tier where both sides can see it is yours. Let the listener hear the create. Then talk about the outcome.


Three questions for this week

  1. Is loyalty tier a characteristic on the individual, or a type we are willing to publish with @baseType and a schema we can open?
  2. Which system receives IndividualCreateEvent, and can we name the callback without saying “the bus”?
  3. Which client is allowed to POST the individual, and which scope says so?

Key takeaways

  1. Two places. A characteristic for a fact. A schema extension for a type, with @type, @baseType, and @schemaLocation.
  2. Hear the create. Register the hub. Keep the event and the href. Do not poll the collection to discover Jane.
  3. Name the caller. OAuth 2.0 for the machine. OpenID Connect for a person in a screen. Intent describes an outcome and stops there.

Terms in this post

Lookup only. The bodies are above.

TermMeaning in this post
hrefThe address of the record the server returned. Not the id.
TMF632Party. The individual is created here.
TMF629Customer. That party as a buyer. Named, not re-taught.
TMF630Design guidelines. Part 1 is the ordinary shape. Part 2 is extension.
@typeOn a plain body, the concrete type. On an extension, the name of your type.
@baseTypeThe type you extended. Here, Individual.
@schemaLocationWhere the extra fields are defined.
characteristicA name and a value on the resource. On the party API, partyCharacteristic.
TMF688Event management. The pattern each domain API follows for its hub.
hubThe subscription. You give a callback. The publisher POSTs events there.
OAuth 2.0How a machine proves it may call the API. A bearer token on the request.
OIDCOpenID Connect. How a person in a screen is identified.
TMF620Product catalog. Named in the intent section. This post does not call it.
TMF679Qualification. Whether she can have the offer. Named, not called.
TMF622Product order. The call this post does not write.
TMF921Intent. An outcome you want. Not the product order that gets you there.
CAMARAWith Open Gateway, a thin network exposure for outside developers. Not this payload.

Architectural field notes, straight to your inbox.

Essays when they publish. The Daily Radar stays on the site.

Leave a Reply

Discover more from TheTechDialogue

Subscribe now to keep reading and get access to the full archive.

Continue reading